Qubx 0.7.3__tar.gz → 0.7.4__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.
Potentially problematic release.
This version of Qubx might be problematic. Click here for more details.
- {qubx-0.7.3 → qubx-0.7.4}/PKG-INFO +1 -1
- {qubx-0.7.3 → qubx-0.7.4}/pyproject.toml +1 -1
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/account.py +49 -1
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/broker.py +117 -273
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/client.py +12 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/handlers/trades.py +4 -4
- qubx-0.7.4/src/qubx/connectors/xlighter/nonce.py +21 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/websocket.py +10 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/context.py +162 -44
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/exceptions.py +4 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/interfaces.py +60 -7
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/mixins/processing.py +16 -2
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/mixins/trading.py +30 -5
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/series.pyi +15 -1
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/series.pyx +108 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/data/readers.py +112 -1
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/emitters/composite.py +17 -1
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/emitters/questdb.py +81 -15
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/exporters/formatters/slack.py +6 -4
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/exporters/slack.py +35 -74
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/gathering/simplest.py +1 -1
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/notifications/__init__.py +5 -5
- qubx-0.7.4/src/qubx/notifications/composite.py +83 -0
- qubx-0.7.4/src/qubx/notifications/slack.py +190 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/pandaz/ta.py +32 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/ta/indicators.pyi +5 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/ta/indicators.pyx +152 -1
- qubx-0.7.4/src/qubx/utils/nonce.py +53 -0
- qubx-0.7.4/src/qubx/utils/ringbuffer.pxd +17 -0
- qubx-0.7.4/src/qubx/utils/ringbuffer.pyi +197 -0
- qubx-0.7.4/src/qubx/utils/ringbuffer.pyx +253 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/factory.py +11 -14
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/runner.py +3 -3
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/textual/app.py +21 -5
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/textual/handlers.py +5 -1
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/textual/init_code.py +3 -6
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/textual/widgets/orders_table.py +4 -5
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/textual/widgets/quotes_table.py +4 -10
- qubx-0.7.4/src/qubx/utils/slack.py +177 -0
- qubx-0.7.3/src/qubx/notifications/composite.py +0 -71
- qubx-0.7.3/src/qubx/notifications/slack.py +0 -213
- {qubx-0.7.3 → qubx-0.7.4}/LICENSE +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/README.md +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/build.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/backtester/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/backtester/account.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/backtester/broker.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/backtester/data.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/backtester/management.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/backtester/ome.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/backtester/optimization.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/backtester/runner.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/backtester/simulator.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/backtester/transfers.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/backtester/utils.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/cli/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/cli/commands.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/cli/deploy.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/cli/misc.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/cli/release.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/cli/tui.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/reader.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/constants.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/data.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/extensions.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/factory.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/handlers/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/handlers/base.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/handlers/orderbook.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/handlers/quote.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/handlers/stats.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/instruments.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/parsers.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/rate_limits.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/reader.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/connectors/xlighter/utils.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/account.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/basics.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/deque.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/detectors/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/detectors/delisting.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/detectors/stale.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/errors.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/helpers.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/initializer.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/loggers.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/lookups.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/metrics.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/mixins/market.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/mixins/utils.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/series.pxd +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/data/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/data/composite.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/data/containers.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/data/helpers.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/data/hft.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/data/registry.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/data/storage.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/data/storages/csv.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/data/storages/questdb.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/data/storages/utils.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/data/tardis.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/data/transformers.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/emitters/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/emitters/base.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/emitters/csv.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/emitters/indicator.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/exporters/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/exporters/composite.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/exporters/formatters/target_position.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/features/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/features/core.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/features/orderbook.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/features/price.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/features/trades.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/features/utils.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/health/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/health/base.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/loggers/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/loggers/csv.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/loggers/factory.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/loggers/mongo.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/math/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/math/stats.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/notifications/throttler.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/pandaz/utils.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/resources/_build.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/restarts/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/restorers/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/restorers/balance.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/restorers/factory.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/restorers/position.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/restorers/signal.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/restorers/state.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/restorers/utils.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/templates/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/templates/base.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/templates/project/template.yml +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/trackers/advanced.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/trackers/sizers.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/charting/orderbook.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/collections.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/hft/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/hft/numba_utils.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/hft/orderbook.pyi +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/hft/orderbook.pyx +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/misc.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/orderbook.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/questdb.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/rate_limiter.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/configs.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/kernel_service.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/textual/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/textual/kernel.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/textual/styles.tcss +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/time.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/version.py +0 -0
- {qubx-0.7.3 → qubx-0.7.4}/src/qubx/utils/websocket_manager.py +0 -0
|
@@ -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.4"
|
|
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"
|
|
@@ -12,9 +12,10 @@ through channel for strategy notification but do not update positions.
|
|
|
12
12
|
|
|
13
13
|
import asyncio
|
|
14
14
|
import time
|
|
15
|
-
from typing import Optional
|
|
15
|
+
from typing import Awaitable, Callable, Optional
|
|
16
16
|
|
|
17
17
|
import numpy as np
|
|
18
|
+
import pandas as pd
|
|
18
19
|
|
|
19
20
|
from qubx import logger
|
|
20
21
|
from qubx.core.account import BasicAccountProcessor
|
|
@@ -28,6 +29,7 @@ from qubx.core.basics import (
|
|
|
28
29
|
TransactionCostsCalculator,
|
|
29
30
|
)
|
|
30
31
|
from qubx.core.interfaces import ISubscriptionManager
|
|
32
|
+
from qubx.core.utils import recognize_timeframe
|
|
31
33
|
from qubx.utils.misc import AsyncThreadLoop
|
|
32
34
|
|
|
33
35
|
from .client import LighterClient
|
|
@@ -268,6 +270,7 @@ class LighterAccountProcessor(BasicAccountProcessor):
|
|
|
268
270
|
await self._subscribe_account_all()
|
|
269
271
|
await self._subscribe_account_all_orders()
|
|
270
272
|
await self._subscribe_user_stats()
|
|
273
|
+
await self._poller(name="sync_orders", coroutine=self._sync_orders, interval="1min")
|
|
271
274
|
|
|
272
275
|
except Exception as e:
|
|
273
276
|
self.__error(f"Failed to start subscriptions: {e}")
|
|
@@ -300,6 +303,51 @@ class LighterAccountProcessor(BasicAccountProcessor):
|
|
|
300
303
|
self.__error(f"Failed to subscribe to user_stats for account {self._lighter_account_index}: {e}")
|
|
301
304
|
raise
|
|
302
305
|
|
|
306
|
+
async def _sync_orders(self):
|
|
307
|
+
now = self.time_provider.time()
|
|
308
|
+
orders = self.get_orders()
|
|
309
|
+
remove_orders = []
|
|
310
|
+
for order_id, order in orders.items():
|
|
311
|
+
if order.status == "NEW" and order.time < now - recognize_timeframe("1min"):
|
|
312
|
+
remove_orders.append(order_id)
|
|
313
|
+
for order_id in remove_orders:
|
|
314
|
+
self.remove_order(order_id)
|
|
315
|
+
|
|
316
|
+
async def _poller(
|
|
317
|
+
self,
|
|
318
|
+
name: str,
|
|
319
|
+
coroutine: Callable[[], Awaitable],
|
|
320
|
+
interval: str,
|
|
321
|
+
backoff: str | None = None,
|
|
322
|
+
):
|
|
323
|
+
sleep_time = pd.Timedelta(interval).total_seconds()
|
|
324
|
+
retries = 0
|
|
325
|
+
|
|
326
|
+
if backoff is not None:
|
|
327
|
+
sleep_time = pd.Timedelta(backoff).total_seconds()
|
|
328
|
+
await asyncio.sleep(sleep_time)
|
|
329
|
+
|
|
330
|
+
while self.channel.control.is_set():
|
|
331
|
+
try:
|
|
332
|
+
await coroutine()
|
|
333
|
+
retries = 0 # Reset retry counter on success
|
|
334
|
+
except Exception as e:
|
|
335
|
+
if not self.channel.control.is_set():
|
|
336
|
+
# If the channel is closed, then ignore all exceptions and exit
|
|
337
|
+
break
|
|
338
|
+
logger.error(f"Unexpected error during account polling: {e}")
|
|
339
|
+
logger.exception(e)
|
|
340
|
+
retries += 1
|
|
341
|
+
if retries >= self.max_retries:
|
|
342
|
+
logger.error(f"Max retries ({self.max_retries}) reached. Stopping poller.")
|
|
343
|
+
break
|
|
344
|
+
finally:
|
|
345
|
+
if not self.channel.control.is_set():
|
|
346
|
+
break
|
|
347
|
+
await asyncio.sleep(min(sleep_time * (2 ** (retries)), 60)) # Exponential backoff capped at 60s
|
|
348
|
+
|
|
349
|
+
logger.debug(f"{name} polling task has been stopped")
|
|
350
|
+
|
|
303
351
|
async def _handle_account_all_message(self, message: dict):
|
|
304
352
|
"""
|
|
305
353
|
Handle account_all WebSocket messages (primary channel).
|
|
@@ -1,20 +1,10 @@
|
|
|
1
|
-
"""
|
|
2
|
-
LighterBroker - IBroker implementation for Lighter exchange.
|
|
3
|
-
|
|
4
|
-
Handles order operations:
|
|
5
|
-
- Order creation (market/limit)
|
|
6
|
-
- Order cancellation
|
|
7
|
-
- Order modification
|
|
8
|
-
- Order tracking
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
1
|
import asyncio
|
|
12
2
|
import uuid
|
|
13
3
|
from typing import Any
|
|
14
4
|
|
|
15
5
|
from qubx import logger
|
|
16
6
|
from qubx.core.basics import CtrlChannel, Instrument, ITimeProvider, Order, OrderSide
|
|
17
|
-
from qubx.core.errors import
|
|
7
|
+
from qubx.core.errors import BaseErrorEvent, ErrorLevel, OrderCreationError, create_error_event
|
|
18
8
|
from qubx.core.exceptions import InvalidOrderParameters, OrderNotFound
|
|
19
9
|
from qubx.core.interfaces import IAccountProcessor, IBroker, IDataProvider
|
|
20
10
|
from qubx.utils.misc import AsyncThreadLoop
|
|
@@ -36,20 +26,8 @@ from .extensions import LighterExchangeAPI
|
|
|
36
26
|
from .instruments import LighterInstrumentLoader
|
|
37
27
|
from .websocket import LighterWebSocketManager
|
|
38
28
|
|
|
39
|
-
# Utils imported as needed
|
|
40
|
-
|
|
41
29
|
|
|
42
30
|
class LighterBroker(IBroker):
|
|
43
|
-
"""
|
|
44
|
-
Broker for Lighter exchange.
|
|
45
|
-
|
|
46
|
-
Supports:
|
|
47
|
-
- Market and limit orders
|
|
48
|
-
- Order cancellation
|
|
49
|
-
- Native order modification (via sign_modify_order)
|
|
50
|
-
- WebSocket order updates (via AccountProcessor)
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
31
|
def __init__(
|
|
54
32
|
self,
|
|
55
33
|
client: LighterClient,
|
|
@@ -90,34 +68,20 @@ class LighterBroker(IBroker):
|
|
|
90
68
|
self.cancel_timeout = cancel_timeout
|
|
91
69
|
self.cancel_retry_interval = cancel_retry_interval
|
|
92
70
|
self.max_cancel_retries = max_cancel_retries
|
|
93
|
-
|
|
94
|
-
# Async thread loop for submitting tasks to client's event loop
|
|
95
71
|
self._async_loop = AsyncThreadLoop(loop)
|
|
96
|
-
|
|
97
|
-
# Track client order IDs and indices
|
|
98
72
|
self._client_order_ids: dict[str, str] = {} # client_id -> exchange_order_id
|
|
99
73
|
self._client_order_indices: dict[str, int] = {} # client_id -> client_order_index
|
|
100
|
-
|
|
101
|
-
# Initialize Lighter-specific extensions
|
|
102
74
|
self._extensions = LighterExchangeAPI(client=self.client, broker=self)
|
|
103
75
|
|
|
104
76
|
@property
|
|
105
77
|
def is_simulated_trading(self) -> bool:
|
|
106
|
-
"""Check if broker is in simulation mode (always False for live)"""
|
|
107
78
|
return False
|
|
108
79
|
|
|
109
80
|
def exchange(self) -> str:
|
|
110
|
-
"""Return exchange name"""
|
|
111
81
|
return "LIGHTER"
|
|
112
82
|
|
|
113
83
|
@property
|
|
114
84
|
def extensions(self) -> LighterExchangeAPI:
|
|
115
|
-
"""
|
|
116
|
-
Access Lighter-specific API extensions.
|
|
117
|
-
|
|
118
|
-
Returns:
|
|
119
|
-
LighterExchangeAPI: Lighter exchange extensions
|
|
120
|
-
"""
|
|
121
85
|
return self._extensions
|
|
122
86
|
|
|
123
87
|
def send_order(
|
|
@@ -131,30 +95,18 @@ class LighterBroker(IBroker):
|
|
|
131
95
|
time_in_force: str = "gtc",
|
|
132
96
|
**options,
|
|
133
97
|
) -> Order:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
Returns:
|
|
148
|
-
Order: Created order object
|
|
149
|
-
|
|
150
|
-
Raises:
|
|
151
|
-
InvalidOrderParameters: If order parameters are invalid
|
|
152
|
-
"""
|
|
153
|
-
# Submit async order creation to event loop and wait for result
|
|
154
|
-
future = self._async_loop.submit(
|
|
155
|
-
self._create_order(instrument, order_side, order_type, amount, price, client_id, time_in_force, **options)
|
|
156
|
-
)
|
|
157
|
-
return future.result()
|
|
98
|
+
return self._async_loop.submit(
|
|
99
|
+
self._create_order(
|
|
100
|
+
instrument=instrument,
|
|
101
|
+
order_side=order_side,
|
|
102
|
+
order_type=order_type,
|
|
103
|
+
amount=amount,
|
|
104
|
+
price=price,
|
|
105
|
+
client_id=client_id,
|
|
106
|
+
time_in_force=time_in_force,
|
|
107
|
+
**options,
|
|
108
|
+
)
|
|
109
|
+
).result()
|
|
158
110
|
|
|
159
111
|
def send_order_async(
|
|
160
112
|
self,
|
|
@@ -167,15 +119,6 @@ class LighterBroker(IBroker):
|
|
|
167
119
|
time_in_force: str = "gtc",
|
|
168
120
|
**options,
|
|
169
121
|
) -> Any:
|
|
170
|
-
"""
|
|
171
|
-
Send order asynchronously.
|
|
172
|
-
|
|
173
|
-
Errors will be sent through the channel.
|
|
174
|
-
|
|
175
|
-
Returns:
|
|
176
|
-
Task/Future that will contain the order
|
|
177
|
-
"""
|
|
178
|
-
|
|
179
122
|
async def _execute_order_with_channel_errors():
|
|
180
123
|
try:
|
|
181
124
|
order = await self._create_order(
|
|
@@ -188,7 +131,43 @@ class LighterBroker(IBroker):
|
|
|
188
131
|
)
|
|
189
132
|
return None
|
|
190
133
|
|
|
191
|
-
return
|
|
134
|
+
return self._async_loop.submit(_execute_order_with_channel_errors())
|
|
135
|
+
|
|
136
|
+
def cancel_order(self, order_id: str) -> bool:
|
|
137
|
+
order = self._find_order(order_id)
|
|
138
|
+
if order is None:
|
|
139
|
+
raise OrderNotFound(f"Order not found: {order_id}")
|
|
140
|
+
return self._async_loop.submit(self._cancel_order(order)).result()
|
|
141
|
+
|
|
142
|
+
def cancel_order_async(self, order_id: str) -> None:
|
|
143
|
+
order = self._find_order(order_id)
|
|
144
|
+
if order is None:
|
|
145
|
+
self._post_cancel_error_to_channel(OrderNotFound(f"Order not found: {order_id}"), order_id)
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
async def _cancel_with_errors():
|
|
149
|
+
try:
|
|
150
|
+
await self._cancel_order(order)
|
|
151
|
+
except Exception as error:
|
|
152
|
+
self._post_cancel_error_to_channel(error, order_id)
|
|
153
|
+
|
|
154
|
+
return self._async_loop.submit(_cancel_with_errors()).result()
|
|
155
|
+
|
|
156
|
+
def cancel_orders(self, instrument: Instrument) -> None:
|
|
157
|
+
orders = self.account.get_orders(instrument=instrument)
|
|
158
|
+
|
|
159
|
+
for order in orders.values():
|
|
160
|
+
try:
|
|
161
|
+
self.cancel_order_async(order.id)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.error(f"Failed to cancel order {order.id}: {e}")
|
|
164
|
+
|
|
165
|
+
def update_order(self, order_id: str, price: float, amount: float) -> Order:
|
|
166
|
+
order = self._find_order(order_id)
|
|
167
|
+
if order is None:
|
|
168
|
+
raise OrderNotFound(f"Order not found: {order_id}")
|
|
169
|
+
future = self._async_loop.submit(self._modify_order(order, price, amount))
|
|
170
|
+
return future.result()
|
|
192
171
|
|
|
193
172
|
async def _create_order(
|
|
194
173
|
self,
|
|
@@ -201,26 +180,6 @@ class LighterBroker(IBroker):
|
|
|
201
180
|
time_in_force: str,
|
|
202
181
|
**options,
|
|
203
182
|
) -> Order:
|
|
204
|
-
"""
|
|
205
|
-
Create order via local signing + WebSocket submission.
|
|
206
|
-
|
|
207
|
-
Args:
|
|
208
|
-
instrument: Instrument to trade
|
|
209
|
-
order_side: Order side
|
|
210
|
-
order_type: Order type
|
|
211
|
-
amount: Order amount
|
|
212
|
-
price: Limit price
|
|
213
|
-
client_id: Client order ID
|
|
214
|
-
time_in_force: Time in force
|
|
215
|
-
**options: Additional parameters
|
|
216
|
-
|
|
217
|
-
Returns:
|
|
218
|
-
Order object
|
|
219
|
-
|
|
220
|
-
Raises:
|
|
221
|
-
InvalidOrderParameters: If parameters are invalid
|
|
222
|
-
"""
|
|
223
|
-
# Validate parameters
|
|
224
183
|
if order_type not in ["market", "limit"]:
|
|
225
184
|
raise InvalidOrderParameters(f"Invalid order type: {order_type}")
|
|
226
185
|
|
|
@@ -251,7 +210,13 @@ class LighterBroker(IBroker):
|
|
|
251
210
|
lighter_tif = tif_map.get(time_in_force.lower(), ORDER_TIME_IN_FORCE_GOOD_TILL_TIME)
|
|
252
211
|
|
|
253
212
|
# Extract additional options
|
|
254
|
-
|
|
213
|
+
order_sign = +1 if order_side == "BUY" else -1
|
|
214
|
+
reduce_only = options.get("reduce_only", None)
|
|
215
|
+
if reduce_only is None:
|
|
216
|
+
if self._is_position_reducing(instrument, amount * order_sign):
|
|
217
|
+
reduce_only = True
|
|
218
|
+
else:
|
|
219
|
+
reduce_only = False
|
|
255
220
|
|
|
256
221
|
# Market orders MUST use IOC (Immediate or Cancel) time in force
|
|
257
222
|
# This is a requirement of Lighter's API
|
|
@@ -312,12 +277,8 @@ class LighterBroker(IBroker):
|
|
|
312
277
|
|
|
313
278
|
logger.info(
|
|
314
279
|
f"Creating order: {order_side} {amount} {instrument.symbol} "
|
|
315
|
-
f"@ {price if price else 'MARKET'} (type={order_type}, tif={time_in_force})"
|
|
280
|
+
f"@ {price if price else 'MARKET'} (type={order_type}, tif={time_in_force}, reduce_only={reduce_only})"
|
|
316
281
|
)
|
|
317
|
-
# logger.debug(
|
|
318
|
-
# f"Decimal conversion: amount={amount} → {base_amount_int} (10^{instrument.size_precision}), "
|
|
319
|
-
# f"price={price} → {price_int} (10^{instrument.price_precision})"
|
|
320
|
-
# )
|
|
321
282
|
|
|
322
283
|
try:
|
|
323
284
|
# Step 1: Sign transaction locally
|
|
@@ -333,6 +294,7 @@ class LighterBroker(IBroker):
|
|
|
333
294
|
reduce_only=int(reduce_only),
|
|
334
295
|
trigger_price=0, # Not using trigger orders
|
|
335
296
|
order_expiry=order_expiry,
|
|
297
|
+
nonce=await self.ws_manager.next_nonce(),
|
|
336
298
|
)
|
|
337
299
|
|
|
338
300
|
if error or tx_info is None:
|
|
@@ -374,204 +336,79 @@ class LighterBroker(IBroker):
|
|
|
374
336
|
logger.error(f"Failed to create order: {e}")
|
|
375
337
|
raise InvalidOrderParameters(f"Order creation failed: {e}") from e
|
|
376
338
|
|
|
377
|
-
def
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
async def _cancel_order(self, order_id: str) -> bool:
|
|
411
|
-
"""
|
|
412
|
-
Cancel order via local signing + WebSocket submission.
|
|
413
|
-
|
|
414
|
-
Args:
|
|
415
|
-
order_id: Order ID to cancel
|
|
416
|
-
|
|
417
|
-
Returns:
|
|
418
|
-
True if successful
|
|
339
|
+
def _find_order(self, order_id: str) -> Order | None:
|
|
340
|
+
# Check if this is a client order ID
|
|
341
|
+
if order_id in self._client_order_ids:
|
|
342
|
+
exchange_order_id = self._client_order_ids[order_id]
|
|
343
|
+
client_id = order_id
|
|
344
|
+
else:
|
|
345
|
+
exchange_order_id = order_id
|
|
346
|
+
client_id = None
|
|
347
|
+
|
|
348
|
+
# Get order details to find market_id and client_id
|
|
349
|
+
orders = self.account.get_orders()
|
|
350
|
+
order = None
|
|
351
|
+
for ord in orders.values():
|
|
352
|
+
if ord.id == exchange_order_id or ord.client_id == order_id:
|
|
353
|
+
order = ord
|
|
354
|
+
if client_id is None and ord.client_id:
|
|
355
|
+
client_id = ord.client_id
|
|
356
|
+
break
|
|
357
|
+
|
|
358
|
+
return order
|
|
359
|
+
|
|
360
|
+
def _find_order_index(self, order: Order) -> int:
|
|
361
|
+
# Get the client_order_index we used during creation
|
|
362
|
+
# If not available, compute it the same way as during creation
|
|
363
|
+
if order.client_id and order.client_id in self._client_order_indices:
|
|
364
|
+
return self._client_order_indices[order.client_id]
|
|
365
|
+
elif order.client_id:
|
|
366
|
+
return abs(hash(order.client_id)) % (10**9)
|
|
367
|
+
elif order.id.isdigit():
|
|
368
|
+
return int(order.id)
|
|
369
|
+
else:
|
|
370
|
+
return abs(hash(order.id)) % (2**56)
|
|
419
371
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
"""
|
|
423
|
-
logger.info(f"Canceling order: {order_id}")
|
|
372
|
+
async def _cancel_order(self, order: Order) -> bool:
|
|
373
|
+
logger.info(f"Canceling order: {order.id}")
|
|
424
374
|
|
|
425
375
|
try:
|
|
426
|
-
# Check if this is a client order ID
|
|
427
|
-
if order_id in self._client_order_ids:
|
|
428
|
-
exchange_order_id = self._client_order_ids[order_id]
|
|
429
|
-
client_id = order_id
|
|
430
|
-
else:
|
|
431
|
-
exchange_order_id = order_id
|
|
432
|
-
client_id = None
|
|
433
|
-
|
|
434
|
-
# Get order details to find market_id and client_id
|
|
435
|
-
orders = self.account.get_orders()
|
|
436
|
-
order = None
|
|
437
|
-
for ord in orders.values():
|
|
438
|
-
if ord.id == exchange_order_id or ord.client_id == order_id:
|
|
439
|
-
order = ord
|
|
440
|
-
if client_id is None and ord.client_id:
|
|
441
|
-
client_id = ord.client_id
|
|
442
|
-
break
|
|
443
|
-
|
|
444
|
-
if order is None:
|
|
445
|
-
raise OrderNotFound(f"Order not found: {order_id}")
|
|
446
|
-
|
|
447
|
-
# Get market_id
|
|
448
376
|
market_id = self.instrument_loader.get_market_id(order.instrument.symbol)
|
|
449
377
|
if market_id is None:
|
|
450
378
|
raise OrderNotFound(f"Market ID not found for {order.instrument.symbol}")
|
|
451
379
|
|
|
452
|
-
|
|
453
|
-
# If not available, compute it the same way as during creation
|
|
454
|
-
if client_id and client_id in self._client_order_indices:
|
|
455
|
-
order_index = self._client_order_indices[client_id]
|
|
456
|
-
elif client_id:
|
|
457
|
-
# Fallback: compute using same algorithm as creation
|
|
458
|
-
order_index = abs(hash(client_id)) % (10**9)
|
|
459
|
-
elif exchange_order_id.isdigit():
|
|
460
|
-
order_index = int(exchange_order_id)
|
|
461
|
-
else:
|
|
462
|
-
# Last resort: hash the exchange_order_id but constrain to 56-bit limit
|
|
463
|
-
order_index = abs(hash(exchange_order_id)) % (2**56)
|
|
464
|
-
|
|
465
|
-
# Step 1: Sign cancellation transaction locally
|
|
380
|
+
order_index = self._find_order_index(order)
|
|
466
381
|
signer = self.client.signer_client
|
|
467
|
-
tx_info, error = signer.sign_cancel_order(
|
|
382
|
+
tx_info, error = signer.sign_cancel_order(
|
|
383
|
+
market_index=market_id, order_index=order_index, nonce=await self.ws_manager.next_nonce()
|
|
384
|
+
)
|
|
468
385
|
|
|
469
386
|
if error or tx_info is None:
|
|
470
387
|
logger.error(f"Order cancellation signing failed: {error}")
|
|
471
388
|
return False
|
|
472
389
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
logger.info(f"Order cancellation submitted via WebSocket: {order_id}")
|
|
390
|
+
await self.ws_manager.send_tx(tx_type=TX_TYPE_CANCEL_ORDER, tx_info=tx_info, tx_id=f"cancel_{order.id}")
|
|
391
|
+
logger.info(f"Order cancellation submitted via WebSocket: {order.id}")
|
|
477
392
|
return True
|
|
478
393
|
|
|
479
394
|
except Exception as e:
|
|
480
|
-
logger.error(f"Failed to cancel order {
|
|
395
|
+
logger.error(f"Failed to cancel order {order.id}: {e}")
|
|
481
396
|
raise OrderNotFound(f"Order cancellation failed: {e}") from e
|
|
482
397
|
|
|
483
|
-
def
|
|
484
|
-
"""
|
|
485
|
-
Cancel all orders for an instrument.
|
|
486
|
-
|
|
487
|
-
Args:
|
|
488
|
-
instrument: Instrument to cancel orders for
|
|
489
|
-
"""
|
|
490
|
-
orders = self.account.get_orders(instrument=instrument)
|
|
491
|
-
|
|
492
|
-
for order_id in orders.keys():
|
|
493
|
-
try:
|
|
494
|
-
self.cancel_order_async(order_id)
|
|
495
|
-
except Exception as e:
|
|
496
|
-
logger.error(f"Failed to cancel order {order_id}: {e}")
|
|
497
|
-
|
|
498
|
-
def update_order(self, order_id: str, price: float, amount: float) -> Order:
|
|
499
|
-
"""
|
|
500
|
-
Update order via native order modification.
|
|
501
|
-
|
|
502
|
-
Uses Lighter's sign_modify_order for atomic order updates.
|
|
503
|
-
|
|
504
|
-
Args:
|
|
505
|
-
order_id: Order ID to update
|
|
506
|
-
price: New price
|
|
507
|
-
amount: New amount
|
|
508
|
-
|
|
509
|
-
Returns:
|
|
510
|
-
Updated order object
|
|
511
|
-
|
|
512
|
-
Raises:
|
|
513
|
-
OrderNotFound: If order not found
|
|
514
|
-
"""
|
|
515
|
-
# Submit async modification to event loop and wait for result
|
|
516
|
-
future = self._async_loop.submit(self._modify_order(order_id, price, amount))
|
|
517
|
-
return future.result()
|
|
518
|
-
|
|
519
|
-
async def _modify_order(self, order_id: str, price: float, amount: float) -> Order:
|
|
520
|
-
"""
|
|
521
|
-
Modify order via local signing + WebSocket submission.
|
|
522
|
-
|
|
523
|
-
Args:
|
|
524
|
-
order_id: Order ID to modify
|
|
525
|
-
price: New price
|
|
526
|
-
amount: New amount
|
|
527
|
-
|
|
528
|
-
Returns:
|
|
529
|
-
Updated order object
|
|
530
|
-
|
|
531
|
-
Raises:
|
|
532
|
-
OrderNotFound: If order not found
|
|
533
|
-
"""
|
|
398
|
+
async def _modify_order(self, order: Order, price: float, amount: float) -> Order:
|
|
534
399
|
try:
|
|
535
|
-
# Check if this is a client order ID
|
|
536
|
-
if order_id in self._client_order_ids:
|
|
537
|
-
exchange_order_id = self._client_order_ids[order_id]
|
|
538
|
-
client_id = order_id
|
|
539
|
-
else:
|
|
540
|
-
exchange_order_id = order_id
|
|
541
|
-
client_id = None
|
|
542
|
-
|
|
543
|
-
# Get order details
|
|
544
|
-
orders = self.account.get_orders()
|
|
545
|
-
order = None
|
|
546
|
-
for ord in orders.values():
|
|
547
|
-
if ord.id == exchange_order_id or ord.client_id == order_id:
|
|
548
|
-
order = ord
|
|
549
|
-
if client_id is None and ord.client_id:
|
|
550
|
-
client_id = ord.client_id
|
|
551
|
-
break
|
|
552
|
-
|
|
553
|
-
if order is None:
|
|
554
|
-
raise OrderNotFound(f"Order not found: {order_id}")
|
|
555
|
-
|
|
556
|
-
# Get market_id
|
|
557
400
|
market_id = self.instrument_loader.get_market_id(order.instrument.symbol)
|
|
558
401
|
if market_id is None:
|
|
559
402
|
raise OrderNotFound(f"Market ID not found for {order.instrument.symbol}")
|
|
560
403
|
|
|
561
|
-
|
|
562
|
-
if client_id and client_id in self._client_order_indices:
|
|
563
|
-
order_index = self._client_order_indices[client_id]
|
|
564
|
-
elif order.id.isdigit():
|
|
565
|
-
order_index = order.id
|
|
566
|
-
else:
|
|
567
|
-
raise OrderNotFound(f"Order index not found for {order_id}")
|
|
404
|
+
order_index = self._find_order_index(order)
|
|
568
405
|
|
|
569
406
|
# Convert price and amount to Lighter's integer format
|
|
570
407
|
instrument = order.instrument
|
|
571
408
|
base_amount_int = int(amount * (10**instrument.size_precision))
|
|
572
409
|
price_int = int(price * (10**instrument.price_precision))
|
|
573
410
|
|
|
574
|
-
logger.debug(f"Modify order {
|
|
411
|
+
logger.debug(f"Modify order {order.id}: amount={order.quantity} → {amount}, price={order.price} → {price}")
|
|
575
412
|
|
|
576
413
|
# Step 1: Sign modification transaction locally
|
|
577
414
|
signer = self.client.signer_client
|
|
@@ -581,13 +418,14 @@ class LighterBroker(IBroker):
|
|
|
581
418
|
base_amount=base_amount_int,
|
|
582
419
|
price=price_int,
|
|
583
420
|
trigger_price=0, # Not using trigger orders
|
|
421
|
+
nonce=await self.ws_manager.next_nonce(),
|
|
584
422
|
)
|
|
585
423
|
|
|
586
424
|
if error or tx_info is None:
|
|
587
425
|
raise OrderNotFound(f"Order modification signing failed: {error}")
|
|
588
426
|
|
|
589
427
|
# Step 2: Submit via WebSocket
|
|
590
|
-
await self.ws_manager.send_tx(tx_type=TX_TYPE_MODIFY_ORDER, tx_info=tx_info, tx_id=f"modify_{
|
|
428
|
+
await self.ws_manager.send_tx(tx_type=TX_TYPE_MODIFY_ORDER, tx_info=tx_info, tx_id=f"modify_{order.id}")
|
|
591
429
|
|
|
592
430
|
# Create updated Order object
|
|
593
431
|
updated_order = Order(
|
|
@@ -604,11 +442,11 @@ class LighterBroker(IBroker):
|
|
|
604
442
|
options=order.options,
|
|
605
443
|
)
|
|
606
444
|
|
|
607
|
-
logger.info(f"Order modification submitted via WebSocket: {
|
|
445
|
+
logger.info(f"Order modification submitted via WebSocket: {order.id}")
|
|
608
446
|
return updated_order
|
|
609
447
|
|
|
610
448
|
except Exception as e:
|
|
611
|
-
logger.error(f"Failed to modify order {
|
|
449
|
+
logger.error(f"Failed to modify order {order.id}: {e}")
|
|
612
450
|
raise OrderNotFound(f"Order modification failed: {e}") from e
|
|
613
451
|
|
|
614
452
|
async def send_orders_batch(
|
|
@@ -762,6 +600,7 @@ class LighterBroker(IBroker):
|
|
|
762
600
|
reduce_only=int(reduce_only),
|
|
763
601
|
trigger_price=0,
|
|
764
602
|
order_expiry=order_expiry,
|
|
603
|
+
nonce=await self.ws_manager.next_nonce(),
|
|
765
604
|
)
|
|
766
605
|
|
|
767
606
|
if error or tx_info is None:
|
|
@@ -812,7 +651,6 @@ class LighterBroker(IBroker):
|
|
|
812
651
|
time_in_force: str,
|
|
813
652
|
**options,
|
|
814
653
|
):
|
|
815
|
-
"""Post order creation error to channel"""
|
|
816
654
|
level = ErrorLevel.MEDIUM
|
|
817
655
|
|
|
818
656
|
if "insufficient" in str(error).lower():
|
|
@@ -838,7 +676,6 @@ class LighterBroker(IBroker):
|
|
|
838
676
|
self.channel.send(create_error_event(error_event))
|
|
839
677
|
|
|
840
678
|
def _post_cancel_error_to_channel(self, error: Exception, order_id: str):
|
|
841
|
-
"""Post order cancellation error to channel"""
|
|
842
679
|
level = ErrorLevel.MEDIUM
|
|
843
680
|
|
|
844
681
|
if "not found" in str(error).lower():
|
|
@@ -847,11 +684,18 @@ class LighterBroker(IBroker):
|
|
|
847
684
|
else:
|
|
848
685
|
logger.error(f"Order cancellation error: {error}")
|
|
849
686
|
|
|
850
|
-
error_event =
|
|
687
|
+
error_event = BaseErrorEvent(
|
|
851
688
|
timestamp=self.time_provider.time(),
|
|
852
689
|
message=f"Failed to cancel order {order_id}: {str(error)}",
|
|
853
690
|
level=level,
|
|
854
|
-
order_id=order_id,
|
|
855
691
|
error=error,
|
|
856
692
|
)
|
|
857
693
|
self.channel.send(create_error_event(error_event))
|
|
694
|
+
|
|
695
|
+
def _is_position_reducing(self, instrument: Instrument, signed_amount: float) -> bool:
|
|
696
|
+
current_position = self.account.get_position(instrument)
|
|
697
|
+
return (
|
|
698
|
+
current_position.quantity > 0 and signed_amount < 0 and abs(signed_amount) <= abs(current_position.quantity)
|
|
699
|
+
) or (
|
|
700
|
+
current_position.quantity < 0 and signed_amount > 0 and abs(signed_amount) <= abs(current_position.quantity)
|
|
701
|
+
)
|