Qubx 0.6.93__tar.gz → 0.6.94__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.6.93 → qubx-0.6.94}/PKG-INFO +1 -1
- {qubx-0.6.93 → qubx-0.6.94}/pyproject.toml +1 -1
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/backtester/simulator.py +15 -1
- qubx-0.6.94/src/qubx/backtester/transfers.py +146 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/xlighter/account.py +127 -237
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/xlighter/constants.py +2 -3
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/xlighter/data.py +199 -97
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/xlighter/factory.py +5 -50
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/xlighter/handlers/__init__.py +2 -0
- qubx-0.6.94/src/qubx/connectors/xlighter/handlers/orderbook.py +120 -0
- qubx-0.6.94/src/qubx/connectors/xlighter/handlers/stats.py +351 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/xlighter/parsers.py +2 -21
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/xlighter/reader.py +2 -1
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/xlighter/websocket.py +183 -99
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/account.py +15 -12
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/basics.py +32 -5
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/context.py +71 -6
- qubx-0.6.94/src/qubx/core/detectors/__init__.py +4 -0
- qubx-0.6.94/src/qubx/core/detectors/delisting.py +81 -0
- qubx-0.6.93/src/qubx/core/stale_data_detector.py → qubx-0.6.94/src/qubx/core/detectors/stale.py +101 -100
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/initializer.py +46 -1
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/interfaces.py +88 -2
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/metrics.py +96 -5
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/mixins/processing.py +13 -17
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/mixins/trading.py +89 -5
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/mixins/universe.py +7 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/data/storages/questdb.py +16 -2
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/orderbook.py +102 -2
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/runner.py +5 -5
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/websocket_manager.py +156 -6
- qubx-0.6.93/src/qubx/connectors/xlighter/handlers/orderbook.py +0 -207
- qubx-0.6.93/src/qubx/connectors/xlighter/orderbook_maintainer.py +0 -314
- {qubx-0.6.93 → qubx-0.6.94}/LICENSE +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/README.md +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/build.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/backtester/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/backtester/account.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/backtester/broker.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/backtester/data.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/backtester/management.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/backtester/ome.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/backtester/optimization.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/backtester/runner.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/backtester/utils.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/cli/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/cli/commands.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/cli/deploy.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/cli/misc.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/cli/release.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/cli/tui.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/reader.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/xlighter/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/xlighter/broker.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/xlighter/client.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/xlighter/extensions.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/xlighter/handlers/base.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/xlighter/handlers/quote.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/xlighter/handlers/trades.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/xlighter/instruments.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/connectors/xlighter/utils.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/deque.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/errors.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/exceptions.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/helpers.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/loggers.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/lookups.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/mixins/market.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/mixins/utils.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/series.pxd +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/series.pyi +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/series.pyx +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/data/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/data/composite.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/data/containers.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/data/helpers.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/data/hft.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/data/readers.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/data/registry.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/data/storage.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/data/storages/csv.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/data/storages/utils.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/data/tardis.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/data/transformers.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/emitters/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/emitters/base.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/emitters/composite.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/emitters/csv.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/emitters/indicator.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/emitters/questdb.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/exporters/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/exporters/composite.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/exporters/formatters/target_position.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/exporters/slack.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/features/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/features/core.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/features/orderbook.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/features/price.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/features/trades.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/features/utils.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/gathering/simplest.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/health/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/health/base.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/loggers/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/loggers/csv.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/loggers/factory.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/loggers/mongo.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/math/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/math/stats.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/notifications/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/notifications/composite.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/notifications/slack.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/notifications/throttler.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/pandaz/ta.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/pandaz/utils.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/resources/_build.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/restarts/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/restorers/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/restorers/balance.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/restorers/factory.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/restorers/position.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/restorers/signal.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/restorers/state.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/restorers/utils.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/templates/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/templates/base.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/templates/project/template.yml +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/trackers/advanced.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/trackers/sizers.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/charting/orderbook.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/collections.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/misc.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/questdb.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/configs.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/kernel_service.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/textual/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/textual/app.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/textual/handlers.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/textual/init_code.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/textual/kernel.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/textual/styles.tcss +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/time.py +0 -0
- {qubx-0.6.93 → qubx-0.6.94}/src/qubx/utils/version.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.6.
|
|
7
|
+
version = "0.6.94"
|
|
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"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Literal
|
|
1
|
+
from typing import Literal, cast
|
|
2
2
|
|
|
3
3
|
import pandas as pd
|
|
4
4
|
from joblib import delayed
|
|
@@ -15,6 +15,7 @@ from qubx.utils.runner.configs import PrefetchConfig
|
|
|
15
15
|
from qubx.utils.time import handle_start_stop, to_utc_naive
|
|
16
16
|
|
|
17
17
|
from .runner import SimulationRunner
|
|
18
|
+
from .transfers import SimulationTransferManager
|
|
18
19
|
from .utils import (
|
|
19
20
|
DataDecls_t,
|
|
20
21
|
ExchangeName_t,
|
|
@@ -300,6 +301,18 @@ def _run_setup(
|
|
|
300
301
|
if enable_inmemory_emitter and emitter is not None:
|
|
301
302
|
emitter_data = emitter.get_dataframe()
|
|
302
303
|
|
|
304
|
+
# - get transfers log
|
|
305
|
+
transfers_log = None
|
|
306
|
+
if hasattr(runner.ctx, "_transfer_manager") and isinstance(
|
|
307
|
+
getattr(runner.ctx, "_transfer_manager"), SimulationTransferManager
|
|
308
|
+
):
|
|
309
|
+
try:
|
|
310
|
+
transfer_manager = cast(SimulationTransferManager, getattr(runner.ctx, "_transfer_manager"))
|
|
311
|
+
transfers_log = transfer_manager.get_transfers_dataframe()
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.error(f"Failed to get transfers log: {e}")
|
|
314
|
+
transfers_log = None
|
|
315
|
+
|
|
303
316
|
return TradingSessionResult(
|
|
304
317
|
setup_id,
|
|
305
318
|
setup.name,
|
|
@@ -319,6 +332,7 @@ def _run_setup(
|
|
|
319
332
|
is_simulation=True,
|
|
320
333
|
author=get_current_user(),
|
|
321
334
|
emitter_data=emitter_data,
|
|
335
|
+
transfers_log=transfers_log,
|
|
322
336
|
)
|
|
323
337
|
except Exception as e:
|
|
324
338
|
logger.error(f"Simulation setup {setup_id} failed with error: {e}")
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from qubx import logger
|
|
7
|
+
from qubx.core.account import CompositeAccountProcessor
|
|
8
|
+
from qubx.core.basics import ITimeProvider
|
|
9
|
+
from qubx.core.interfaces import ITransferManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SimulationTransferManager(ITransferManager):
|
|
13
|
+
"""
|
|
14
|
+
Transfer manager for simulation mode.
|
|
15
|
+
|
|
16
|
+
Handles fund transfers between exchanges by directly manipulating account balances.
|
|
17
|
+
All transfers are instant and tracked in a DataFrame for export to results.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_account: CompositeAccountProcessor
|
|
21
|
+
_time: ITimeProvider
|
|
22
|
+
_transfers: list[dict[str, Any]]
|
|
23
|
+
|
|
24
|
+
def __init__(self, account_processor: CompositeAccountProcessor, time_provider: ITimeProvider):
|
|
25
|
+
"""
|
|
26
|
+
Initialize simulation transfer manager.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
account_processor: Account processor (typically CompositeAccountProcessor)
|
|
30
|
+
time_provider: Time provider for timestamping transfers
|
|
31
|
+
"""
|
|
32
|
+
self._account = account_processor
|
|
33
|
+
self._time = time_provider
|
|
34
|
+
self._transfers = []
|
|
35
|
+
|
|
36
|
+
def transfer_funds(self, from_exchange: str, to_exchange: str, currency: str, amount: float) -> str:
|
|
37
|
+
"""
|
|
38
|
+
Transfer funds between exchanges (instant in simulation).
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
from_exchange: Source exchange identifier
|
|
42
|
+
to_exchange: Destination exchange identifier
|
|
43
|
+
currency: Currency to transfer
|
|
44
|
+
amount: Amount to transfer
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
str: Transaction ID (UUID)
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If exchanges not found or insufficient funds
|
|
51
|
+
"""
|
|
52
|
+
# Generate transaction ID
|
|
53
|
+
transaction_id = f"sim_{uuid.uuid4().hex[:12]}"
|
|
54
|
+
|
|
55
|
+
# Get timestamp
|
|
56
|
+
timestamp = self._time.time()
|
|
57
|
+
|
|
58
|
+
# Get individual processors
|
|
59
|
+
try:
|
|
60
|
+
from_processor = self._account.get_account_processor(from_exchange)
|
|
61
|
+
to_processor = self._account.get_account_processor(to_exchange)
|
|
62
|
+
except (KeyError, AttributeError) as e:
|
|
63
|
+
raise ValueError(f"Exchange not found: {e}")
|
|
64
|
+
|
|
65
|
+
# Validate sufficient funds
|
|
66
|
+
from_balances = from_processor.get_balances()
|
|
67
|
+
if currency not in from_balances:
|
|
68
|
+
raise ValueError(f"Currency '{currency}' not found in {from_exchange}")
|
|
69
|
+
|
|
70
|
+
available = from_balances[currency].free
|
|
71
|
+
if available < amount:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"Insufficient funds in {from_exchange}: "
|
|
74
|
+
f"{available:.8f} {currency} available, {amount:.8f} {currency} requested"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Execute transfer (instant balance manipulation)
|
|
78
|
+
from_balances[currency].total -= amount
|
|
79
|
+
from_balances[currency].free -= amount
|
|
80
|
+
|
|
81
|
+
to_balances = to_processor.get_balances()
|
|
82
|
+
to_balances[currency].total += amount
|
|
83
|
+
to_balances[currency].free += amount
|
|
84
|
+
|
|
85
|
+
# Record transfer
|
|
86
|
+
transfer_record = {
|
|
87
|
+
"transaction_id": transaction_id,
|
|
88
|
+
"timestamp": timestamp,
|
|
89
|
+
"from_exchange": from_exchange,
|
|
90
|
+
"to_exchange": to_exchange,
|
|
91
|
+
"currency": currency,
|
|
92
|
+
"amount": amount,
|
|
93
|
+
"status": "completed", # Always completed in simulation
|
|
94
|
+
}
|
|
95
|
+
self._transfers.append(transfer_record)
|
|
96
|
+
|
|
97
|
+
logger.debug(f"[SimTransfer] {amount:.8f} {currency} {from_exchange} → {to_exchange} (ID: {transaction_id})")
|
|
98
|
+
|
|
99
|
+
return transaction_id
|
|
100
|
+
|
|
101
|
+
def get_transfer_status(self, transaction_id: str) -> dict[str, Any]:
|
|
102
|
+
"""
|
|
103
|
+
Get the status of a transfer.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
transaction_id: Transaction ID
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
dict[str, Any]: Transfer status information
|
|
110
|
+
"""
|
|
111
|
+
# Find transfer
|
|
112
|
+
for transfer in self._transfers:
|
|
113
|
+
if transfer["transaction_id"] == transaction_id:
|
|
114
|
+
return transfer.copy()
|
|
115
|
+
|
|
116
|
+
# Not found
|
|
117
|
+
return {
|
|
118
|
+
"transaction_id": transaction_id,
|
|
119
|
+
"status": "not_found",
|
|
120
|
+
"error": f"Transaction {transaction_id} not found",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
def get_transfers(self) -> dict[str, dict[str, Any]]:
|
|
124
|
+
"""
|
|
125
|
+
Get all transfers as a dictionary.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
dict[str, dict[str, Any]]: Dictionary mapping transaction IDs to transfer info
|
|
129
|
+
"""
|
|
130
|
+
return {t["transaction_id"]: t for t in self._transfers}
|
|
131
|
+
|
|
132
|
+
def get_transfers_dataframe(self) -> pd.DataFrame:
|
|
133
|
+
"""
|
|
134
|
+
Get all transfers as a pandas DataFrame.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
pd.DataFrame: DataFrame with columns [transaction_id, timestamp, from_exchange,
|
|
138
|
+
to_exchange, currency, amount, status]
|
|
139
|
+
"""
|
|
140
|
+
if not self._transfers:
|
|
141
|
+
# Return empty DataFrame with correct schema
|
|
142
|
+
return pd.DataFrame(
|
|
143
|
+
columns=["transaction_id", "from_exchange", "to_exchange", "currency", "amount", "status"] # type: ignore
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return pd.DataFrame(self._transfers).set_index("timestamp")
|
|
@@ -19,6 +19,7 @@ import numpy as np
|
|
|
19
19
|
from qubx import logger
|
|
20
20
|
from qubx.core.account import BasicAccountProcessor
|
|
21
21
|
from qubx.core.basics import (
|
|
22
|
+
ZERO_COSTS,
|
|
22
23
|
CtrlChannel,
|
|
23
24
|
DataType,
|
|
24
25
|
Deal,
|
|
@@ -65,7 +66,7 @@ class LighterAccountProcessor(BasicAccountProcessor):
|
|
|
65
66
|
time_provider: ITimeProvider,
|
|
66
67
|
loop: asyncio.AbstractEventLoop,
|
|
67
68
|
base_currency: str = "USDC",
|
|
68
|
-
tcc: TransactionCostsCalculator = None,
|
|
69
|
+
tcc: TransactionCostsCalculator | None = None,
|
|
69
70
|
initial_capital: float = 100_000,
|
|
70
71
|
max_retries: int = 10,
|
|
71
72
|
connection_timeout: int = 30,
|
|
@@ -88,8 +89,6 @@ class LighterAccountProcessor(BasicAccountProcessor):
|
|
|
88
89
|
connection_timeout: Connection timeout in seconds
|
|
89
90
|
"""
|
|
90
91
|
if tcc is None:
|
|
91
|
-
from qubx.core.basics import ZERO_COSTS
|
|
92
|
-
|
|
93
92
|
tcc = ZERO_COSTS
|
|
94
93
|
|
|
95
94
|
super().__init__(
|
|
@@ -121,12 +120,20 @@ class LighterAccountProcessor(BasicAccountProcessor):
|
|
|
121
120
|
self._auth_token_expiry: Optional[int] = None
|
|
122
121
|
self._processed_tx_hashes: set[str] = set() # Track processed transaction hashes
|
|
123
122
|
|
|
123
|
+
self._account_stats_initialized = False
|
|
124
|
+
|
|
124
125
|
logger.info(f"Initialized LighterAccountProcessor for account {account_id}")
|
|
125
126
|
|
|
126
127
|
def set_subscription_manager(self, manager: ISubscriptionManager) -> None:
|
|
127
128
|
"""Set the subscription manager (required by interface)"""
|
|
128
129
|
self._subscription_manager = manager
|
|
129
130
|
|
|
131
|
+
def get_total_capital(self, exchange: str | None = None) -> float:
|
|
132
|
+
if not self._account_stats_initialized:
|
|
133
|
+
self._async_loop.submit(self._start_subscriptions())
|
|
134
|
+
self._wait_for_account_stats_initialized()
|
|
135
|
+
return super().get_total_capital(exchange)
|
|
136
|
+
|
|
130
137
|
def start(self):
|
|
131
138
|
"""Start WebSocket subscriptions for account data"""
|
|
132
139
|
if self._is_running:
|
|
@@ -142,33 +149,13 @@ class LighterAccountProcessor(BasicAccountProcessor):
|
|
|
142
149
|
# Start subscription tasks using AsyncThreadLoop
|
|
143
150
|
logger.info("Starting Lighter account subscriptions")
|
|
144
151
|
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
if not self._account_stats_initialized:
|
|
153
|
+
# Submit connection and subscription tasks to the event loop
|
|
154
|
+
self._async_loop.submit(self._start_subscriptions())
|
|
155
|
+
self._wait_for_account_stats_initialized()
|
|
147
156
|
|
|
148
157
|
logger.info("Lighter account subscriptions started")
|
|
149
158
|
|
|
150
|
-
async def _start_subscriptions(self):
|
|
151
|
-
"""Connect to WebSocket and start all subscriptions"""
|
|
152
|
-
try:
|
|
153
|
-
# Ensure WebSocket is connected
|
|
154
|
-
if not self.ws_manager.is_connected:
|
|
155
|
-
logger.info("Connecting to Lighter WebSocket...")
|
|
156
|
-
await self.ws_manager.connect()
|
|
157
|
-
logger.info("Connected to Lighter WebSocket")
|
|
158
|
-
|
|
159
|
-
# Generate auth token for authenticated channels
|
|
160
|
-
await self._generate_auth_token()
|
|
161
|
-
|
|
162
|
-
# Start all subscriptions
|
|
163
|
-
await self._subscribe_account_all()
|
|
164
|
-
await self._subscribe_account_all_orders()
|
|
165
|
-
await self._subscribe_user_stats()
|
|
166
|
-
|
|
167
|
-
except Exception as e:
|
|
168
|
-
logger.error(f"Failed to start subscriptions: {e}")
|
|
169
|
-
self._is_running = False
|
|
170
|
-
raise
|
|
171
|
-
|
|
172
159
|
def stop(self):
|
|
173
160
|
"""Stop all WebSocket subscriptions"""
|
|
174
161
|
if not self._is_running:
|
|
@@ -183,164 +170,134 @@ class LighterAccountProcessor(BasicAccountProcessor):
|
|
|
183
170
|
|
|
184
171
|
self._subscription_tasks.clear()
|
|
185
172
|
self._is_running = False
|
|
186
|
-
|
|
187
173
|
logger.info("Lighter account subscriptions stopped")
|
|
188
174
|
|
|
189
|
-
|
|
175
|
+
def process_deals(self, instrument: Instrument, deals: list[Deal], is_snapshot: bool = False) -> None:
|
|
190
176
|
"""
|
|
191
|
-
|
|
177
|
+
Override process_deals to track fees WITHOUT updating positions.
|
|
178
|
+
|
|
179
|
+
In Lighter, positions are synced directly from account_all channel
|
|
180
|
+
(single source of truth for quantity and avg_entry_price). However,
|
|
181
|
+
we still need to track fees/commissions from deals.
|
|
192
182
|
|
|
193
|
-
|
|
194
|
-
- account_all/{account_id}
|
|
195
|
-
- account_all_orders/{account_id}
|
|
196
|
-
- user_stats/{account_id}
|
|
183
|
+
This prevents double position updates while ensuring commission tracking.
|
|
197
184
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
185
|
+
Args:
|
|
186
|
+
instrument: The instrument for the deals
|
|
187
|
+
deals: List of Deal objects
|
|
188
|
+
is_snapshot: Whether this is a snapshot or incremental update
|
|
201
189
|
"""
|
|
202
|
-
|
|
203
|
-
|
|
190
|
+
# Do NOT call super().process_deals() - that would update positions
|
|
191
|
+
# Instead, manually track fees for the position
|
|
192
|
+
if not deals:
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
position = self.get_position(instrument)
|
|
196
|
+
|
|
197
|
+
for deal in deals:
|
|
198
|
+
# Track commission from the deal
|
|
199
|
+
if deal.fee_amount and deal.fee_amount > 0:
|
|
200
|
+
# Add fee to position's commission tracking
|
|
201
|
+
position.commissions += deal.fee_amount
|
|
204
202
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
203
|
+
logger.debug(
|
|
204
|
+
f"Tracked fee for {instrument.symbol}: {deal.fee_amount:.6f} {deal.fee_currency} "
|
|
205
|
+
f"(total commissions: {position.commissions:.6f})"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
logger.debug(
|
|
209
|
+
f"Processed {len(deals)} deal(s) for {instrument.symbol} - fees tracked, positions synced from account_all"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def process_order(self, order: Order, update_locked_value: bool = True) -> None:
|
|
213
|
+
"""
|
|
214
|
+
Override process_order to handle Lighter's server-assigned order IDs.
|
|
215
|
+
|
|
216
|
+
Lighter assigns server IDs different from our client_id. When an order
|
|
217
|
+
update arrives with a new server ID but matching client_id, we need to
|
|
218
|
+
migrate the order from client_id key to server_id key while preserving
|
|
219
|
+
the same object instance (for external references).
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
order: Order update from WebSocket
|
|
223
|
+
update_locked_value: Whether to update locked capital tracking
|
|
224
|
+
"""
|
|
225
|
+
# Check if order exists under client_id (migration case)
|
|
226
|
+
if order.client_id and order.client_id in self._active_orders:
|
|
227
|
+
# Get the existing order stored under client_id
|
|
228
|
+
existing_order = self._active_orders[order.client_id]
|
|
229
|
+
|
|
230
|
+
logger.debug(f"Migrating order: client_id={order.client_id} → server_id={order.id}")
|
|
231
|
+
|
|
232
|
+
# Remove from old location
|
|
233
|
+
self._active_orders.pop(order.client_id)
|
|
209
234
|
|
|
210
|
-
|
|
211
|
-
|
|
235
|
+
# Store it under the new server ID before base class processing
|
|
236
|
+
# This allows base class merge logic to find and update it in place
|
|
237
|
+
self._active_orders[order.id] = existing_order
|
|
212
238
|
|
|
213
|
-
if
|
|
214
|
-
|
|
239
|
+
# Also migrate locked capital tracking if present
|
|
240
|
+
if order.client_id in self._locked_capital_by_order:
|
|
241
|
+
locked_value = self._locked_capital_by_order.pop(order.client_id)
|
|
242
|
+
self._locked_capital_by_order[order.id] = locked_value
|
|
215
243
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
self._auth_token_expiry = int(time.time()) + 10 * 60
|
|
244
|
+
# Let base class handle the rest (merge, store, lock/unlock, etc.)
|
|
245
|
+
# The base class will now find the existing order under order.id and merge in place
|
|
246
|
+
super().process_order(order, update_locked_value)
|
|
220
247
|
|
|
221
|
-
|
|
248
|
+
def _wait_for_account_stats_initialized(self):
|
|
249
|
+
max_wait_time = 20.0 # seconds
|
|
250
|
+
elapsed = 0.0
|
|
251
|
+
interval = 0.1
|
|
252
|
+
while not self._account_stats_initialized:
|
|
253
|
+
if elapsed >= max_wait_time:
|
|
254
|
+
raise TimeoutError(f"Account stats were not initialized within {max_wait_time} seconds")
|
|
255
|
+
time.sleep(interval)
|
|
256
|
+
elapsed += interval
|
|
257
|
+
|
|
258
|
+
async def _start_subscriptions(self):
|
|
259
|
+
"""Connect to WebSocket and start all subscriptions"""
|
|
260
|
+
try:
|
|
261
|
+
# Ensure WebSocket is connected
|
|
262
|
+
if not self.ws_manager.is_connected:
|
|
263
|
+
logger.info("Connecting to Lighter WebSocket...")
|
|
264
|
+
await self.ws_manager.connect()
|
|
265
|
+
logger.info("Connected to Lighter WebSocket")
|
|
266
|
+
|
|
267
|
+
# Start all subscriptions
|
|
268
|
+
await self._subscribe_account_all()
|
|
269
|
+
await self._subscribe_account_all_orders()
|
|
270
|
+
await self._subscribe_user_stats()
|
|
222
271
|
|
|
223
272
|
except Exception as e:
|
|
224
|
-
logger.error(f"Failed to
|
|
273
|
+
logger.error(f"Failed to start subscriptions: {e}")
|
|
274
|
+
self._is_running = False
|
|
225
275
|
raise
|
|
226
276
|
|
|
227
277
|
async def _subscribe_account_all(self):
|
|
228
|
-
"""
|
|
229
|
-
Subscribe to account_all channel for positions and trades (primary channel).
|
|
230
|
-
|
|
231
|
-
Requires authentication token.
|
|
232
|
-
|
|
233
|
-
This is the single source of truth for positions and trade history.
|
|
234
|
-
Updates positions directly from position data, sends trades as Deals
|
|
235
|
-
through channel for strategy notification.
|
|
236
|
-
|
|
237
|
-
Message format:
|
|
238
|
-
{
|
|
239
|
-
"account": 225671,
|
|
240
|
-
"channel": "account_all:225671",
|
|
241
|
-
"type": "update/account_all",
|
|
242
|
-
"positions": {
|
|
243
|
-
"24": {
|
|
244
|
-
"market_id": 24,
|
|
245
|
-
"sign": -1, # 1 for long, -1 for short
|
|
246
|
-
"position": "1.00",
|
|
247
|
-
"avg_entry_price": "40.1342",
|
|
248
|
-
...
|
|
249
|
-
}
|
|
250
|
-
},
|
|
251
|
-
"trades": {
|
|
252
|
-
"24": [
|
|
253
|
-
{
|
|
254
|
-
"trade_id": 225067334,
|
|
255
|
-
"market_id": 24,
|
|
256
|
-
"size": "1.00",
|
|
257
|
-
"price": "40.1342",
|
|
258
|
-
"timestamp": 1760287839079,
|
|
259
|
-
...
|
|
260
|
-
}
|
|
261
|
-
]
|
|
262
|
-
},
|
|
263
|
-
"funding_histories": {}
|
|
264
|
-
}
|
|
265
|
-
"""
|
|
266
|
-
channel = f"account_all/{self._lighter_account_index}"
|
|
267
|
-
logger.info(f"Subscribing to {channel} (with auth)")
|
|
268
|
-
|
|
269
278
|
try:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
channel=channel, handler=self._handle_account_all_message, auth=self._auth_token
|
|
273
|
-
)
|
|
274
|
-
logger.info(f"Successfully subscribed to {channel}")
|
|
279
|
+
await self.ws_manager.subscribe_account_all(self._lighter_account_index, self._handle_account_all_message)
|
|
280
|
+
logger.info(f"Subscribed to account_all for account {self._lighter_account_index}")
|
|
275
281
|
except Exception as e:
|
|
276
|
-
logger.error(f"Failed to subscribe to {
|
|
282
|
+
logger.error(f"Failed to subscribe to account_all for account {self._lighter_account_index}: {e}")
|
|
277
283
|
raise
|
|
278
284
|
|
|
279
285
|
async def _subscribe_account_all_orders(self):
|
|
280
|
-
"""
|
|
281
|
-
Subscribe to account_all_orders channel for order updates across all markets.
|
|
282
|
-
|
|
283
|
-
Requires authentication token.
|
|
284
|
-
|
|
285
|
-
Message format:
|
|
286
|
-
{
|
|
287
|
-
"channel": "account_all_orders:225671",
|
|
288
|
-
"type": "update/account_all_orders",
|
|
289
|
-
"orders": {
|
|
290
|
-
"24": [ # market_index
|
|
291
|
-
{
|
|
292
|
-
"order_id": "7036874567748225",
|
|
293
|
-
"status": "filled",
|
|
294
|
-
...
|
|
295
|
-
}
|
|
296
|
-
]
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
"""
|
|
300
|
-
channel = f"account_all_orders/{self._lighter_account_index}"
|
|
301
|
-
logger.info(f"Subscribing to {channel} (with auth)")
|
|
302
|
-
|
|
303
286
|
try:
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
channel=channel, handler=self._handle_account_all_orders_message, auth=self._auth_token
|
|
287
|
+
await self.ws_manager.subscribe_account_all_orders(
|
|
288
|
+
self._lighter_account_index, self._handle_account_all_orders_message
|
|
307
289
|
)
|
|
308
|
-
logger.info(f"
|
|
290
|
+
logger.info(f"Subscribed to account_all_orders for account {self._lighter_account_index}")
|
|
309
291
|
except Exception as e:
|
|
310
|
-
logger.error(f"Failed to subscribe to {
|
|
292
|
+
logger.error(f"Failed to subscribe to account_all_orders for account {self._lighter_account_index}: {e}")
|
|
311
293
|
raise
|
|
312
294
|
|
|
313
295
|
async def _subscribe_user_stats(self):
|
|
314
|
-
"""
|
|
315
|
-
Subscribe to user_stats channel for account statistics.
|
|
316
|
-
|
|
317
|
-
Requires authentication token.
|
|
318
|
-
|
|
319
|
-
Message format:
|
|
320
|
-
{
|
|
321
|
-
"channel": "user_stats:225671",
|
|
322
|
-
"type": "update/user_stats",
|
|
323
|
-
"stats": {
|
|
324
|
-
"collateral": "998.888700",
|
|
325
|
-
"portfolio_value": "998.901500",
|
|
326
|
-
"available_balance": "990.920600",
|
|
327
|
-
"leverage": "0.04",
|
|
328
|
-
"margin_usage": "0.80",
|
|
329
|
-
...
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
"""
|
|
333
|
-
channel = f"user_stats/{self._lighter_account_index}"
|
|
334
|
-
logger.info(f"Subscribing to {channel} (with auth)")
|
|
335
|
-
|
|
336
296
|
try:
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
channel=channel, handler=self._handle_user_stats_message, auth=self._auth_token
|
|
340
|
-
)
|
|
341
|
-
logger.info(f"Successfully subscribed to {channel}")
|
|
297
|
+
await self.ws_manager.subscribe_user_stats(self._lighter_account_index, self._handle_user_stats_message)
|
|
298
|
+
logger.info(f"Subscribed to user_stats for account {self._lighter_account_index}")
|
|
342
299
|
except Exception as e:
|
|
343
|
-
logger.error(f"Failed to subscribe to {
|
|
300
|
+
logger.error(f"Failed to subscribe to user_stats for account {self._lighter_account_index}: {e}")
|
|
344
301
|
raise
|
|
345
302
|
|
|
346
303
|
async def _handle_account_all_message(self, message: dict):
|
|
@@ -369,6 +326,8 @@ class LighterAccountProcessor(BasicAccountProcessor):
|
|
|
369
326
|
# Sync quantity and position_avg_price from Lighter's authoritative data
|
|
370
327
|
position.quantity = pos_state.quantity
|
|
371
328
|
position.position_avg_price = pos_state.avg_entry_price
|
|
329
|
+
position.position_avg_price_funds = pos_state.avg_entry_price
|
|
330
|
+
position.r_pnl = pos_state.realized_pnl
|
|
372
331
|
|
|
373
332
|
# Update market price for unrealized PnL recalculation
|
|
374
333
|
# Use the avg_entry_price as a reference if no better price available
|
|
@@ -393,15 +352,15 @@ class LighterAccountProcessor(BasicAccountProcessor):
|
|
|
393
352
|
f"fee={deal.fee_amount:.6f} (id={deal.id})"
|
|
394
353
|
)
|
|
395
354
|
|
|
355
|
+
# Funding payments are handled by the data provider, so I commented them here to avoid double sending
|
|
396
356
|
# Send funding payments through channel
|
|
397
|
-
for instrument, payments in funding_payments.items():
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
)
|
|
357
|
+
# for instrument, payments in funding_payments.items():
|
|
358
|
+
# for payment in payments:
|
|
359
|
+
# # Send: (instrument, DataType.FUNDING_PAYMENT, payment, False)
|
|
360
|
+
# self.channel.send((instrument, DataType.FUNDING_PAYMENT, payment, False))
|
|
361
|
+
# logger.debug(
|
|
362
|
+
# f"Sent funding payment: {instrument.symbol} rate={payment.funding_rate:.6f} at {payment.time}"
|
|
363
|
+
# )
|
|
405
364
|
|
|
406
365
|
except Exception as e:
|
|
407
366
|
logger.error(f"Error handling account_all message: {e}")
|
|
@@ -453,79 +412,10 @@ class LighterAccountProcessor(BasicAccountProcessor):
|
|
|
453
412
|
f"free={balance.free:.2f}, locked={balance.locked:.2f}"
|
|
454
413
|
)
|
|
455
414
|
|
|
415
|
+
if not self._account_stats_initialized:
|
|
416
|
+
self._account_stats_initialized = True
|
|
417
|
+
logger.debug("Account stats initialized")
|
|
418
|
+
|
|
456
419
|
except Exception as e:
|
|
457
420
|
logger.error(f"Error handling user_stats message: {e}")
|
|
458
421
|
logger.exception(e)
|
|
459
|
-
|
|
460
|
-
def process_deals(self, instrument: Instrument, deals: list[Deal], is_snapshot: bool = False) -> None:
|
|
461
|
-
"""
|
|
462
|
-
Override process_deals to track fees WITHOUT updating positions.
|
|
463
|
-
|
|
464
|
-
In Lighter, positions are synced directly from account_all channel
|
|
465
|
-
(single source of truth for quantity and avg_entry_price). However,
|
|
466
|
-
we still need to track fees/commissions from deals.
|
|
467
|
-
|
|
468
|
-
This prevents double position updates while ensuring commission tracking.
|
|
469
|
-
|
|
470
|
-
Args:
|
|
471
|
-
instrument: The instrument for the deals
|
|
472
|
-
deals: List of Deal objects
|
|
473
|
-
is_snapshot: Whether this is a snapshot or incremental update
|
|
474
|
-
"""
|
|
475
|
-
# Do NOT call super().process_deals() - that would update positions
|
|
476
|
-
# Instead, manually track fees for the position
|
|
477
|
-
if not deals:
|
|
478
|
-
return
|
|
479
|
-
|
|
480
|
-
position = self.get_position(instrument)
|
|
481
|
-
|
|
482
|
-
for deal in deals:
|
|
483
|
-
# Track commission from the deal
|
|
484
|
-
if deal.fee_amount and deal.fee_amount > 0:
|
|
485
|
-
# Add fee to position's commission tracking
|
|
486
|
-
position.commissions += deal.fee_amount
|
|
487
|
-
|
|
488
|
-
logger.debug(
|
|
489
|
-
f"Tracked fee for {instrument.symbol}: {deal.fee_amount:.6f} {deal.fee_currency} "
|
|
490
|
-
f"(total commissions: {position.commissions:.6f})"
|
|
491
|
-
)
|
|
492
|
-
|
|
493
|
-
logger.debug(
|
|
494
|
-
f"Processed {len(deals)} deal(s) for {instrument.symbol} - fees tracked, positions synced from account_all"
|
|
495
|
-
)
|
|
496
|
-
|
|
497
|
-
def process_order(self, order: Order, update_locked_value: bool = True) -> None:
|
|
498
|
-
"""
|
|
499
|
-
Override process_order to handle Lighter's server-assigned order IDs.
|
|
500
|
-
|
|
501
|
-
Lighter assigns server IDs different from our client_id. When an order
|
|
502
|
-
update arrives with a new server ID but matching client_id, we need to
|
|
503
|
-
migrate the order from client_id key to server_id key while preserving
|
|
504
|
-
the same object instance (for external references).
|
|
505
|
-
|
|
506
|
-
Args:
|
|
507
|
-
order: Order update from WebSocket
|
|
508
|
-
update_locked_value: Whether to update locked capital tracking
|
|
509
|
-
"""
|
|
510
|
-
# Check if order exists under client_id (migration case)
|
|
511
|
-
if order.client_id and order.client_id in self._active_orders:
|
|
512
|
-
# Get the existing order stored under client_id
|
|
513
|
-
existing_order = self._active_orders[order.client_id]
|
|
514
|
-
|
|
515
|
-
logger.debug(f"Migrating order: client_id={order.client_id} → server_id={order.id}")
|
|
516
|
-
|
|
517
|
-
# Remove from old location
|
|
518
|
-
self._active_orders.pop(order.client_id)
|
|
519
|
-
|
|
520
|
-
# Store it under the new server ID before base class processing
|
|
521
|
-
# This allows base class merge logic to find and update it in place
|
|
522
|
-
self._active_orders[order.id] = existing_order
|
|
523
|
-
|
|
524
|
-
# Also migrate locked capital tracking if present
|
|
525
|
-
if order.client_id in self._locked_capital_by_order:
|
|
526
|
-
locked_value = self._locked_capital_by_order.pop(order.client_id)
|
|
527
|
-
self._locked_capital_by_order[order.id] = locked_value
|
|
528
|
-
|
|
529
|
-
# Let base class handle the rest (merge, store, lock/unlock, etc.)
|
|
530
|
-
# The base class will now find the existing order under order.id and merge in place
|
|
531
|
-
super().process_order(order, update_locked_value)
|