Qubx 1.1.2.dev1__tar.gz → 1.1.2.dev3__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-1.1.2.dev1 → qubx-1.1.2.dev3}/PKG-INFO +1 -1
- qubx-1.1.2.dev3/src/qubx/_version.py +24 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/management.py +29 -4
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/tui.py +35 -5
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/okx/account.py +51 -1
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/basics.py +11 -3
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/metrics.py +91 -8
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/cache.py +4 -2
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/storages/questdb.py +4 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/transformers.py +1 -1
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/app.py +26 -2
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/handlers.py +8 -3
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/init_code.py +16 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/styles.tcss +15 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/widgets/__init__.py +2 -1
- qubx-1.1.2.dev3/src/qubx/utils/runner/textual/widgets/account_summary.py +47 -0
- qubx-1.1.2.dev1/src/qubx/_version.py +0 -34
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/.gitignore +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/LICENSE +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/README.md +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/pyproject.toml +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/scripts/build.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/_nb_magic.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/account.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/broker.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/data.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/iteratedstream.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/ome.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/optimization.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/runner.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/simulator.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/transfers.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/utils.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/commands.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/deploy.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/misc.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/release.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/resolver.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/s3.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/theme.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/user_config.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/config.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/okx/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/okx/broker.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/okx/okx.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/registry.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/account.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/context.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/detectors/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/detectors/delisting.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/detectors/stale.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/errors.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/exceptions.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/helpers.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/initializer.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/interfaces.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/loggers.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/lookups.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/mixins/market.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/mixins/processing.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/mixins/universe.py +2 -2
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/mixins/utils.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/series.pxd +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/series.pyi +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/series.pyx +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/utils.pyi +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/utils.pyx +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/containers.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/guards.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/registry.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/storage.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/storages/ccxt.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/storages/csv.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/storages/handy.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/storages/multi.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/storages/stub.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/storages/utils.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/emitters/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/emitters/base.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/emitters/composite.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/emitters/csv.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/emitters/indicator.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/emitters/questdb.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/exporters/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/exporters/composite.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/exporters/formatters/target_position.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/exporters/slack.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/gathering/simplest.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/health/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/health/base.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/health/dummy.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/health/server.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/loggers/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/loggers/csv.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/loggers/factory.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/loggers/mongo.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/notifications/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/notifications/composite.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/notifications/slack.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/notifications/throttler.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/pandaz/stats.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/pandaz/ta.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/pandaz/utils.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/plugins/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/plugins/loader.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/_build.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restarts/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restorers/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restorers/balance.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restorers/factory.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restorers/position.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restorers/signal.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restorers/state.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restorers/utils.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/state/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/state/dummy.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/state/redis.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/ta/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/base.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/project/template.yml +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.claude/skills/qubx-cli/SKILL.md +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.claude/skills/qubx-indicators/SKILL.md +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.claude/skills/simulation-explorer/SKILL.md +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.claude/skills/strategy-release/SKILL.md +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.github/workflows/ci.yml.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.gitignore.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.python-version +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.vscode/launch.json +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.vscode/settings.json +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.vscode/tasks.json +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/CLAUDE.md.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/README.md.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/configs/.gitkeep +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/justfile.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/pyproject.toml.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/research/.gitkeep +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/src/{{ strategy_name }}/cli.py.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/template.yml +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/tests/test_strategy.py.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/trackers/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/trackers/advanced.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/trackers/composite.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/trackers/sizers.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/charting/orderbook.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/collections.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/hft/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/hft/numba_utils.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/hft/orderbook.pyi +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/hft/orderbook.pyx +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/misc.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/nonce.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/ntp.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/orderbook.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/questdb.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/rate_limiter.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/results.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/ringbuffer.pxd +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/ringbuffer.pyi +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/ringbuffer.pyx +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/configs.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/kernel_service.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/runner.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/__init__.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/kernel.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/s3.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/slack.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/throttler.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/time.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/websocket_manager.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
- {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/tests/strategies/obi_trader/src/obi_trader/models/obi_trader.py +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '1.1.2.dev3'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 1, 2, 'dev3')
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -573,7 +573,7 @@ class BacktestStorage:
|
|
|
573
573
|
if self._is_cloud:
|
|
574
574
|
self._setup_cloud_duckdb()
|
|
575
575
|
|
|
576
|
-
def get_log(self, backtest_id: str, config_name: str | None = None) -> str | None:
|
|
576
|
+
def get_log(self, backtest_id: str, config_name: str | None = None, max_lines: int = 1000) -> tuple[str, bool] | None:
|
|
577
577
|
"""
|
|
578
578
|
Read the log file for a backtest run.
|
|
579
579
|
|
|
@@ -581,14 +581,29 @@ class BacktestStorage:
|
|
|
581
581
|
backtest_id: Relative path within base_path.
|
|
582
582
|
config_name: Config name (used for log filename).
|
|
583
583
|
If None, searches for any .log file.
|
|
584
|
+
max_lines: Maximum number of lines to return (from the end). 0 = unlimited.
|
|
584
585
|
|
|
585
586
|
Returns:
|
|
586
|
-
|
|
587
|
+
(log_content, truncated) tuple, or None if not found.
|
|
587
588
|
"""
|
|
588
589
|
run_dir = f"{self.base_path}{backtest_id.strip('/')}/"
|
|
589
590
|
|
|
590
591
|
if config_name:
|
|
591
592
|
log_path = f"{run_dir}{config_name}.log"
|
|
593
|
+
elif self._is_cloud:
|
|
594
|
+
try:
|
|
595
|
+
from pyarrow.fs import FileSelector, FileType
|
|
596
|
+
|
|
597
|
+
from qubx.utils.s3 import strip_scheme
|
|
598
|
+
|
|
599
|
+
client = S3Client(storage_options=self._storage_options)
|
|
600
|
+
entries = client.fs.get_file_info(FileSelector(strip_scheme(run_dir)))
|
|
601
|
+
log_files = [e.path for e in entries if e.type == FileType.File and e.path.endswith(".log")]
|
|
602
|
+
if not log_files:
|
|
603
|
+
return None
|
|
604
|
+
log_path = f"s3://{log_files[0]}"
|
|
605
|
+
except Exception:
|
|
606
|
+
return None
|
|
592
607
|
else:
|
|
593
608
|
try:
|
|
594
609
|
files_df = self._conn.execute(f"SELECT file FROM glob('{run_dir}*.log')").df()
|
|
@@ -598,20 +613,30 @@ class BacktestStorage:
|
|
|
598
613
|
except Exception:
|
|
599
614
|
return None
|
|
600
615
|
|
|
616
|
+
def _tail(content: str) -> tuple[str, bool]:
|
|
617
|
+
if not max_lines:
|
|
618
|
+
return content, False
|
|
619
|
+
lines = content.splitlines()
|
|
620
|
+
if len(lines) <= max_lines:
|
|
621
|
+
return content, False
|
|
622
|
+
return "\n".join(lines[-max_lines:]), True
|
|
623
|
+
|
|
601
624
|
if self._is_cloud:
|
|
602
625
|
try:
|
|
603
626
|
from qubx.utils.s3 import strip_scheme
|
|
604
627
|
|
|
605
628
|
client = S3Client(storage_options=self._storage_options)
|
|
606
629
|
with client.fs.open_input_stream(strip_scheme(log_path)) as f:
|
|
607
|
-
return f.read().decode("utf-8")
|
|
630
|
+
return _tail(f.read().decode("utf-8"))
|
|
608
631
|
except Exception:
|
|
609
632
|
return None
|
|
610
633
|
else:
|
|
611
634
|
from pathlib import Path
|
|
612
635
|
|
|
613
636
|
p = Path(log_path)
|
|
614
|
-
|
|
637
|
+
if not p.is_file():
|
|
638
|
+
return None
|
|
639
|
+
return _tail(p.read_text(encoding="utf-8", errors="replace"))
|
|
615
640
|
|
|
616
641
|
def export_backtests_to_markdown(self, backtest_id: str, path: str, tags: tuple[str] | None = None):
|
|
617
642
|
if tsr := self.load(backtest_id):
|
|
@@ -844,6 +844,8 @@ class DetailScreen(Screen):
|
|
|
844
844
|
self._result_info = result_info
|
|
845
845
|
self._storage = storage
|
|
846
846
|
self._load_id = result_info.get("load_id", "")
|
|
847
|
+
self._pending_copy: bool = False
|
|
848
|
+
self._pending_copy_timer: Timer | None = None
|
|
847
849
|
|
|
848
850
|
def compose(self) -> ComposeResult:
|
|
849
851
|
name = self._result_info.get("name", self._load_id)
|
|
@@ -854,7 +856,7 @@ class DetailScreen(Screen):
|
|
|
854
856
|
with VerticalScroll(id="detail-scroll"):
|
|
855
857
|
yield Markdown("*Loading…*", id="detail-report")
|
|
856
858
|
with TabPane("Logs", id="tab-logs"):
|
|
857
|
-
yield RichLog(id="detail-log", wrap=True, highlight=True)
|
|
859
|
+
yield RichLog(id="detail-log", wrap=True, highlight=True, markup=True)
|
|
858
860
|
yield Footer()
|
|
859
861
|
|
|
860
862
|
def on_mount(self) -> None:
|
|
@@ -875,16 +877,19 @@ class DetailScreen(Screen):
|
|
|
875
877
|
|
|
876
878
|
# - load log file
|
|
877
879
|
config_name = self._result_info.get("config_name")
|
|
878
|
-
|
|
880
|
+
log_result = self._storage.get_log(self._load_id, config_name)
|
|
879
881
|
|
|
880
|
-
self.app.call_from_thread(self._populate, report,
|
|
882
|
+
self.app.call_from_thread(self._populate, report, log_result)
|
|
881
883
|
|
|
882
|
-
def _populate(self, report: str,
|
|
884
|
+
def _populate(self, report: str, log_result: tuple[str, bool] | None) -> None:
|
|
883
885
|
"""UI-thread callback: fill in the tabs."""
|
|
884
886
|
self.query_one("#detail-report", Markdown).update(report)
|
|
885
887
|
|
|
886
888
|
log_widget = self.query_one("#detail-log", RichLog)
|
|
887
|
-
if
|
|
889
|
+
if log_result:
|
|
890
|
+
log_content, truncated = log_result
|
|
891
|
+
if truncated:
|
|
892
|
+
log_widget.write("[bold yellow]Log truncated — showing last 1000 lines[/]\n")
|
|
888
893
|
for line in log_content.splitlines():
|
|
889
894
|
log_widget.write(line)
|
|
890
895
|
else:
|
|
@@ -910,6 +915,31 @@ class DetailScreen(Screen):
|
|
|
910
915
|
def action_go_back(self) -> None:
|
|
911
916
|
self.app.pop_screen()
|
|
912
917
|
|
|
918
|
+
def on_key(self, event) -> None:
|
|
919
|
+
if event.key == "c":
|
|
920
|
+
if self._pending_copy:
|
|
921
|
+
self._pending_copy = False
|
|
922
|
+
if self._pending_copy_timer:
|
|
923
|
+
self._pending_copy_timer.stop()
|
|
924
|
+
self._pending_copy_timer = None
|
|
925
|
+
self.app.copy_to_clipboard(str(self._load_id))
|
|
926
|
+
self.app.notify(f"Copied: {self._load_id}", timeout=3)
|
|
927
|
+
else:
|
|
928
|
+
self._pending_copy = True
|
|
929
|
+
self._pending_copy_timer = self.set_timer(
|
|
930
|
+
0.5, self._cancel_pending_copy, name="cc_timeout"
|
|
931
|
+
)
|
|
932
|
+
event.prevent_default()
|
|
933
|
+
event.stop()
|
|
934
|
+
elif self._pending_copy:
|
|
935
|
+
self._cancel_pending_copy()
|
|
936
|
+
|
|
937
|
+
def _cancel_pending_copy(self) -> None:
|
|
938
|
+
self._pending_copy = False
|
|
939
|
+
if self._pending_copy_timer:
|
|
940
|
+
self._pending_copy_timer.stop()
|
|
941
|
+
self._pending_copy_timer = None
|
|
942
|
+
|
|
913
943
|
|
|
914
944
|
class BacktestBrowserApp(App):
|
|
915
945
|
"""Main TUI application for browsing backtest results"""
|
|
@@ -7,7 +7,7 @@ from qubx.connectors.ccxt.utils import (
|
|
|
7
7
|
ccxt_extract_deals_from_exec,
|
|
8
8
|
ccxt_find_instrument,
|
|
9
9
|
)
|
|
10
|
-
from qubx.core.basics import CtrlChannel, Instrument
|
|
10
|
+
from qubx.core.basics import AssetBalance, CtrlChannel, Instrument
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class OkxAccountProcessor(CcxtAccountProcessor):
|
|
@@ -57,6 +57,56 @@ class OkxAccountProcessor(CcxtAccountProcessor):
|
|
|
57
57
|
),
|
|
58
58
|
)
|
|
59
59
|
|
|
60
|
+
async def _update_balance(self) -> None:
|
|
61
|
+
"""OKX balance override: use cashBal instead of eq (equity) to avoid double-counting unrealized PnL.
|
|
62
|
+
|
|
63
|
+
CCXT maps OKX's `eq` (equity = cashBal + unrealizedPnL) to balance `total`.
|
|
64
|
+
Since get_total_capital() adds position market values (= unrealized PnL) on top,
|
|
65
|
+
using `eq` would double-count unrealized PnL. We use `cashBal` instead.
|
|
66
|
+
"""
|
|
67
|
+
balances_raw = await self.exchange_manager.exchange.fetch_balance()
|
|
68
|
+
|
|
69
|
+
# Extract cashBal from raw OKX response instead of CCXT-parsed eq
|
|
70
|
+
balances: list[AssetBalance] = []
|
|
71
|
+
raw_details = balances_raw.get("info", {}).get("data", [{}])[0].get("details", [])
|
|
72
|
+
for detail in raw_details:
|
|
73
|
+
cash_bal = float(detail.get("cashBal", 0) or 0)
|
|
74
|
+
if not cash_bal:
|
|
75
|
+
continue
|
|
76
|
+
frozen_bal = float(detail.get("frozenBal", 0) or 0)
|
|
77
|
+
currency = detail["ccy"]
|
|
78
|
+
balances.append(
|
|
79
|
+
AssetBalance(
|
|
80
|
+
exchange=self.exchange,
|
|
81
|
+
currency=currency,
|
|
82
|
+
free=cash_bal - frozen_bal,
|
|
83
|
+
locked=frozen_bal,
|
|
84
|
+
total=cash_bal,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
current_balances = self.get_balances()
|
|
89
|
+
|
|
90
|
+
# remove balances that are not there anymore
|
|
91
|
+
current_currencies = {b.currency for b in current_balances}
|
|
92
|
+
new_currencies = {b.currency for b in balances}
|
|
93
|
+
for currency in current_currencies - new_currencies:
|
|
94
|
+
self.update_balance(currency, 0, 0)
|
|
95
|
+
|
|
96
|
+
# update current balances
|
|
97
|
+
for balance in balances:
|
|
98
|
+
self.update_balance(currency=balance.currency, total=balance.total, locked=balance.locked)
|
|
99
|
+
|
|
100
|
+
# update required instruments that we need to subscribe to
|
|
101
|
+
currencies = [b.currency for b in self.get_balances()]
|
|
102
|
+
instruments = [
|
|
103
|
+
self._get_instrument_for_currency(c) for c in currencies if c.upper() != self.base_currency.upper()
|
|
104
|
+
]
|
|
105
|
+
self._required_instruments.update(instruments)
|
|
106
|
+
|
|
107
|
+
# fetch tickers for instruments that don't have recent price updates
|
|
108
|
+
await self._fetch_missing_tickers(instruments)
|
|
109
|
+
|
|
60
110
|
async def _subscribe_instruments(self, instruments: list) -> None:
|
|
61
111
|
"""Skip SPOT instruments on OKX.F - futures connector only handles swap contracts."""
|
|
62
112
|
await super()._subscribe_instruments([i for i in instruments if i.is_futures()])
|
|
@@ -936,7 +936,8 @@ class Position:
|
|
|
936
936
|
|
|
937
937
|
if not np.isnan(price):
|
|
938
938
|
u_pnl = self.unrealized_pnl()
|
|
939
|
-
|
|
939
|
+
# r_pnl already includes cumulative funding
|
|
940
|
+
self.pnl = u_pnl + self.r_pnl
|
|
940
941
|
if self.instrument.is_futures():
|
|
941
942
|
# for derivatives market value of the position is the current unrealized PnL
|
|
942
943
|
self.market_value = u_pnl
|
|
@@ -955,7 +956,12 @@ class Position:
|
|
|
955
956
|
|
|
956
957
|
def unrealized_pnl(self) -> float:
|
|
957
958
|
if not np.isnan(self.last_update_price):
|
|
958
|
-
return
|
|
959
|
+
return (
|
|
960
|
+
self.quantity
|
|
961
|
+
* self._qty_multiplier
|
|
962
|
+
* (self.last_update_price - self.position_avg_price)
|
|
963
|
+
/ self.last_update_conversion_rate
|
|
964
|
+
) # type: ignore
|
|
959
965
|
return 0.0
|
|
960
966
|
|
|
961
967
|
def apply_funding_payment(self, funding_payment: FundingPayment, mark_price: float) -> float:
|
|
@@ -1021,7 +1027,9 @@ class Position:
|
|
|
1021
1027
|
funds_release = self.market_value_funds
|
|
1022
1028
|
if to_remain != 0 and self.quantity != 0 and np.sign(to_remain) == d:
|
|
1023
1029
|
qty_to_release = max(self.quantity - to_remain, 0) if d > 0 else min(self.quantity - to_remain, 0)
|
|
1024
|
-
funds_release =
|
|
1030
|
+
funds_release = (
|
|
1031
|
+
qty_to_release * self._qty_multiplier * self.last_update_price / self.last_update_conversion_rate
|
|
1032
|
+
)
|
|
1025
1033
|
return abs(funds_release)
|
|
1026
1034
|
|
|
1027
1035
|
@staticmethod
|
|
@@ -685,6 +685,18 @@ class TradingSessionResult:
|
|
|
685
685
|
return df
|
|
686
686
|
return df.loc[start:stop]
|
|
687
687
|
|
|
688
|
+
def _rebase_cumulative_columns(pfl: pd.DataFrame) -> pd.DataFrame:
|
|
689
|
+
"""Rebase cumulative counter columns (_Funding, _Commissions) so the slice starts from zero.
|
|
690
|
+
Note: _PnL is NOT rebased because it includes unrealized PnL which depends on current positions."""
|
|
691
|
+
if pfl is None or pfl.empty:
|
|
692
|
+
return pfl
|
|
693
|
+
pfl = pfl.copy()
|
|
694
|
+
for pattern in ["_Funding", "_Commissions"]:
|
|
695
|
+
cols = [c for c in pfl.columns if c.endswith(pattern)]
|
|
696
|
+
if cols:
|
|
697
|
+
pfl[cols] = pfl[cols] - pfl[cols].iloc[0]
|
|
698
|
+
return pfl
|
|
699
|
+
|
|
688
700
|
start = pd.Timestamp(key.start) if key.start is not None else None
|
|
689
701
|
stop = pd.Timestamp(key.stop) if key.stop is not None else None
|
|
690
702
|
|
|
@@ -696,6 +708,10 @@ class TradingSessionResult:
|
|
|
696
708
|
else:
|
|
697
709
|
capital = self.capital
|
|
698
710
|
|
|
711
|
+
sliced_portfolio = _slice_df(self.portfolio_log)
|
|
712
|
+
if start is not None and sliced_portfolio is not None and not sliced_portfolio.empty:
|
|
713
|
+
sliced_portfolio = _rebase_cumulative_columns(sliced_portfolio)
|
|
714
|
+
|
|
699
715
|
tsr = TradingSessionResult(
|
|
700
716
|
id=self.id,
|
|
701
717
|
name=self.name,
|
|
@@ -706,7 +722,7 @@ class TradingSessionResult:
|
|
|
706
722
|
capital=capital,
|
|
707
723
|
base_currency=self.base_currency,
|
|
708
724
|
commissions=self.commissions,
|
|
709
|
-
portfolio_log=
|
|
725
|
+
portfolio_log=sliced_portfolio,
|
|
710
726
|
executions_log=_slice_df(self.executions_log),
|
|
711
727
|
signals_log=_slice_df(self.signals_log),
|
|
712
728
|
targets_log=_slice_df(self.targets_log),
|
|
@@ -719,9 +735,7 @@ class TradingSessionResult:
|
|
|
719
735
|
emitter_data=_slice_df(self.emitter_data.set_index("timestamp")).reset_index()
|
|
720
736
|
if self.emitter_data is not None
|
|
721
737
|
else None,
|
|
722
|
-
transfers_log=_slice_df(self.transfers_log
|
|
723
|
-
if self.transfers_log is not None
|
|
724
|
-
else None,
|
|
738
|
+
transfers_log=_slice_df(self.transfers_log) if self.transfers_log is not None else None,
|
|
725
739
|
)
|
|
726
740
|
tsr.qubx_version = self.qubx_version
|
|
727
741
|
tsr.description = self.description
|
|
@@ -904,10 +918,75 @@ class TradingSessionResult:
|
|
|
904
918
|
return pd.DataFrame(dtype=float)
|
|
905
919
|
return calculate_funding_per_symbol(self.portfolio_log, start, stop)
|
|
906
920
|
|
|
907
|
-
def get_funding_per_asset(
|
|
921
|
+
def get_funding_per_asset(
|
|
922
|
+
self, start: OptTimestamp = None, stop: OptTimestamp = None, filter_zero: bool = True
|
|
923
|
+
) -> pd.DataFrame:
|
|
908
924
|
if self.portfolio_log.empty:
|
|
909
925
|
return pd.DataFrame(dtype=float)
|
|
910
|
-
return calculate_funding_per_asset(self.portfolio_log, start, stop)
|
|
926
|
+
return calculate_funding_per_asset(self.portfolio_log, start, stop, filter_zero)
|
|
927
|
+
|
|
928
|
+
def get_funding_total(self, start: OptTimestamp = None, stop: OptTimestamp = None) -> pd.Series:
|
|
929
|
+
return self.get_funding_per_asset(start, stop).sum(axis=1).rename("funding_total")
|
|
930
|
+
|
|
931
|
+
def get_funding_pnl(self, start: OptTimestamp = None, stop: OptTimestamp = None) -> pd.Series:
|
|
932
|
+
return self.get_funding_per_asset(start, stop).iloc[-1].sort_values(ascending=False).rename("funding_pnl")
|
|
933
|
+
|
|
934
|
+
def get_asset_pnl(
|
|
935
|
+
self,
|
|
936
|
+
asset: str,
|
|
937
|
+
start: OptTimestamp = None,
|
|
938
|
+
stop: OptTimestamp = None,
|
|
939
|
+
commission_factor: float = 1.0,
|
|
940
|
+
pct_from_initial_capital: bool = False,
|
|
941
|
+
include_funding: bool = False,
|
|
942
|
+
) -> pd.Series:
|
|
943
|
+
portfolio = self.portfolio_log
|
|
944
|
+
start = start or self.start
|
|
945
|
+
stop = stop or self.stop
|
|
946
|
+
init_capital = self.get_total_capital()
|
|
947
|
+
pnl = portfolio.filter(regex=f".*:{asset}USD.*_PnL").loc[start:stop].sum(axis=1).cumsum()
|
|
948
|
+
if not include_funding:
|
|
949
|
+
funding = portfolio.filter(regex=f".*:{asset}USD.*_Funding").loc[start:stop].sum(axis=1)
|
|
950
|
+
if len(funding) > 0:
|
|
951
|
+
funding_delta = funding.diff().fillna(funding.iloc[0])
|
|
952
|
+
pnl -= funding_delta.cumsum()
|
|
953
|
+
if commission_factor:
|
|
954
|
+
comm = portfolio.filter(regex=f".*{asset}.*_Commissions").loc[start:stop].sum(axis=1).cumsum()
|
|
955
|
+
pnl -= comm * commission_factor
|
|
956
|
+
return pnl / init_capital * 100 if pct_from_initial_capital else pnl
|
|
957
|
+
|
|
958
|
+
def get_pnl_per_asset(
|
|
959
|
+
self,
|
|
960
|
+
start: OptTimestamp = None,
|
|
961
|
+
stop: OptTimestamp = None,
|
|
962
|
+
pct_from_initial_capital: bool = False,
|
|
963
|
+
drop_zero: bool = True,
|
|
964
|
+
include_funding: bool = False,
|
|
965
|
+
) -> pd.Series:
|
|
966
|
+
start = start or self.start
|
|
967
|
+
stop = stop or self.stop
|
|
968
|
+
assets = set()
|
|
969
|
+
for symbol in self.symbols:
|
|
970
|
+
parts = symbol.split(":")
|
|
971
|
+
if len(parts) != 2:
|
|
972
|
+
continue
|
|
973
|
+
exchange, symbol = parts
|
|
974
|
+
instrument = lookup.find_symbol(exchange, symbol)
|
|
975
|
+
if instrument:
|
|
976
|
+
assets.add(instrument.asset)
|
|
977
|
+
asset_to_pnl = {}
|
|
978
|
+
for asset in assets:
|
|
979
|
+
pnl = self.get_asset_pnl(asset, start, stop, pct_from_initial_capital=pct_from_initial_capital, include_funding=include_funding)
|
|
980
|
+
if pnl is not None and not pnl.empty:
|
|
981
|
+
last_pnl = pnl.iloc[-1]
|
|
982
|
+
if drop_zero and last_pnl == 0:
|
|
983
|
+
continue
|
|
984
|
+
asset_to_pnl[asset] = last_pnl
|
|
985
|
+
return pd.Series(asset_to_pnl).sort_values(ascending=False)
|
|
986
|
+
|
|
987
|
+
def get_position_asset(self, asset: str, start: OptTimestamp = None, stop: OptTimestamp = None) -> pd.DataFrame:
|
|
988
|
+
pos = self.portfolio_log.filter(regex=f".*{asset}.*_Pos")
|
|
989
|
+
return pos.rename(columns=lambda x: x.replace("_Pos", ""))
|
|
911
990
|
|
|
912
991
|
def performance(self) -> dict[str, float]:
|
|
913
992
|
"""
|
|
@@ -2023,7 +2102,7 @@ def calculate_leverage_per_symbol(
|
|
|
2023
2102
|
df.columns = [col.split(":")[-1] for col in df.columns]
|
|
2024
2103
|
|
|
2025
2104
|
if drop_zero_leverage:
|
|
2026
|
-
df = df.loc[:, df.
|
|
2105
|
+
df = df.loc[:, df.ne(0).any(axis=0)]
|
|
2027
2106
|
|
|
2028
2107
|
return cast(pd.DataFrame, df)
|
|
2029
2108
|
|
|
@@ -2105,6 +2184,7 @@ def calculate_funding_per_asset(
|
|
|
2105
2184
|
portfolio_log: pd.DataFrame,
|
|
2106
2185
|
start: OptTimestamp = None,
|
|
2107
2186
|
stop: OptTimestamp = None,
|
|
2187
|
+
filter_zero: bool = True,
|
|
2108
2188
|
) -> pd.DataFrame:
|
|
2109
2189
|
"""
|
|
2110
2190
|
Calculate funding for each asset in the trading session.
|
|
@@ -2120,7 +2200,10 @@ def calculate_funding_per_asset(
|
|
|
2120
2200
|
col_to_asset[col] = instr.asset if instr else col
|
|
2121
2201
|
|
|
2122
2202
|
# Group by asset and sum
|
|
2123
|
-
|
|
2203
|
+
df = df.rename(columns=col_to_asset).T.groupby(level=0).sum().T
|
|
2204
|
+
if filter_zero:
|
|
2205
|
+
df = df.loc[:, df.ne(0).any(axis=0)]
|
|
2206
|
+
return df
|
|
2124
2207
|
|
|
2125
2208
|
|
|
2126
2209
|
def calculate_turnover(
|
|
@@ -270,16 +270,18 @@ def _merge_batches(existing: pa.RecordBatch, incoming: pa.RecordBatch, time_col_
|
|
|
270
270
|
[
|
|
271
271
|
pa.Table.from_batches([existing]),
|
|
272
272
|
pa.Table.from_batches([incoming]),
|
|
273
|
-
]
|
|
273
|
+
],
|
|
274
|
+
promote_options="permissive",
|
|
274
275
|
)
|
|
275
276
|
time_col_name = tbl.schema.field(time_col_idx).name
|
|
276
277
|
tbl = tbl.sort_by(time_col_name)
|
|
277
278
|
|
|
279
|
+
merged_schema = tbl.schema
|
|
278
280
|
pdf = tbl.to_pandas()
|
|
279
281
|
dedup_cols = [time_col_name] + [f.name for f in existing.schema if pa.types.is_string(f.type) or pa.types.is_large_string(f.type)]
|
|
280
282
|
pdf = pdf.drop_duplicates(subset=dedup_cols, keep="last")
|
|
281
283
|
|
|
282
|
-
batch = pa.RecordBatch.from_pandas(pdf, schema=
|
|
284
|
+
batch = pa.RecordBatch.from_pandas(pdf, schema=merged_schema, preserve_index=False)
|
|
283
285
|
return batch
|
|
284
286
|
|
|
285
287
|
|
|
@@ -318,6 +318,10 @@ class xLTableMetaInfo:
|
|
|
318
318
|
("binance", "cmfutures"): ("binance.cm", "future"),
|
|
319
319
|
("binance", "margin"): ("binance.pm", "swap"),
|
|
320
320
|
("gateio", "swap"): ("gateio.f", "swap"),
|
|
321
|
+
("kraken", "swap"): ("kraken.f", "swap"),
|
|
322
|
+
("hyperliquid", "swap"): ("hyperliquid.f", "swap"),
|
|
323
|
+
("bybit", "swap"): ("bybit.f", "swap"),
|
|
324
|
+
("okx", "swap"): ("okx.f", "swap"),
|
|
321
325
|
}
|
|
322
326
|
|
|
323
327
|
_DTYPE_FIX = {
|
|
@@ -119,7 +119,7 @@ class PandasFrame(IDataTransformer):
|
|
|
119
119
|
tbl = tbl.append_column(pa.field("symbol", pa.large_string()), sym_col)
|
|
120
120
|
tables.append(tbl)
|
|
121
121
|
|
|
122
|
-
tbl = pa.concat_tables(tables)
|
|
122
|
+
tbl = pa.concat_tables(tables, promote_options="permissive")
|
|
123
123
|
df = tbl.to_pandas()
|
|
124
124
|
df[t_name] = pd.to_datetime(df[t_name])
|
|
125
125
|
return df.set_index([t_name, "symbol"]).sort_index()
|
|
@@ -7,6 +7,7 @@ import concurrent.futures
|
|
|
7
7
|
import sys
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
+
import yaml
|
|
10
11
|
from rich.text import Text
|
|
11
12
|
from textual.app import App, ComposeResult, ScreenStackError
|
|
12
13
|
from textual.binding import Binding
|
|
@@ -20,7 +21,7 @@ from qubx.cli.theme import QUBX_DARK
|
|
|
20
21
|
from .handlers import KernelEventHandler
|
|
21
22
|
from .init_code import generate_init_code, generate_mock_init_code
|
|
22
23
|
from .kernel import IPyKernel
|
|
23
|
-
from .widgets import CommandInput, DebugLog, OrdersTable, PositionsTable, QuotesTable, ReplOutput
|
|
24
|
+
from .widgets import AccountSummary, CommandInput, DebugLog, OrdersTable, PositionsTable, QuotesTable, ReplOutput
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class TextualStrategyApp(App[None]):
|
|
@@ -80,8 +81,13 @@ class TextualStrategyApp(App[None]):
|
|
|
80
81
|
self.no_notifiers = no_notifiers
|
|
81
82
|
self.no_exporters = no_exporters
|
|
82
83
|
self.kernel = kernel if kernel is not None else IPyKernel()
|
|
84
|
+
|
|
85
|
+
# Derive app title from config name or strategy class
|
|
86
|
+
self.title = self._resolve_title(config_file)
|
|
87
|
+
|
|
83
88
|
self.output: ReplOutput
|
|
84
89
|
self.input: CommandInput
|
|
90
|
+
self.account_summary: AccountSummary
|
|
85
91
|
self.positions_panel: Vertical
|
|
86
92
|
self.positions_table: PositionsTable
|
|
87
93
|
self.orders_panel: Vertical
|
|
@@ -102,13 +108,29 @@ class TextualStrategyApp(App[None]):
|
|
|
102
108
|
self._original_stderr = None
|
|
103
109
|
self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
|
104
110
|
|
|
111
|
+
@staticmethod
|
|
112
|
+
def _resolve_title(config_file: Path) -> str:
|
|
113
|
+
"""Derive window title from config 'name' field, falling back to strategy class name."""
|
|
114
|
+
try:
|
|
115
|
+
with open(config_file) as f:
|
|
116
|
+
cfg = yaml.safe_load(f) or {}
|
|
117
|
+
if name := cfg.get("name"):
|
|
118
|
+
return str(name)
|
|
119
|
+
if strategy := cfg.get("strategy"):
|
|
120
|
+
return str(strategy).rsplit(".", 1)[-1]
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
return config_file.stem
|
|
124
|
+
|
|
105
125
|
async def on_mount(self) -> None:
|
|
106
126
|
"""Initialize the app when mounted."""
|
|
107
127
|
self.register_theme(QUBX_DARK)
|
|
108
128
|
self.theme = QUBX_DARK.name
|
|
109
129
|
|
|
110
130
|
# Setup event handler
|
|
111
|
-
self.event_handler = KernelEventHandler(
|
|
131
|
+
self.event_handler = KernelEventHandler(
|
|
132
|
+
self.output, self.positions_table, self.orders_table, self.quotes_table, self.account_summary
|
|
133
|
+
)
|
|
112
134
|
|
|
113
135
|
# Add welcome message
|
|
114
136
|
self.output.write(Text("Qubx Strategy Runner", style="bold cyan"))
|
|
@@ -239,6 +261,8 @@ class TextualStrategyApp(App[None]):
|
|
|
239
261
|
def compose(self) -> ComposeResult:
|
|
240
262
|
"""Compose the TUI layout."""
|
|
241
263
|
yield Header(show_clock=True)
|
|
264
|
+
self.account_summary = AccountSummary(id="account-summary")
|
|
265
|
+
yield self.account_summary
|
|
242
266
|
with Vertical(id="content-wrapper"):
|
|
243
267
|
with Horizontal(id="main-container"):
|
|
244
268
|
# Output on the left
|
|
@@ -9,7 +9,7 @@ from rich.text import Text
|
|
|
9
9
|
|
|
10
10
|
from qubx import logger
|
|
11
11
|
|
|
12
|
-
from .widgets import OrdersTable, PositionsTable, QuotesTable, ReplOutput
|
|
12
|
+
from .widgets import AccountSummary, OrdersTable, PositionsTable, QuotesTable, ReplOutput
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class KernelEventHandler:
|
|
@@ -21,6 +21,7 @@ class KernelEventHandler:
|
|
|
21
21
|
positions_table: PositionsTable,
|
|
22
22
|
orders_table: OrdersTable,
|
|
23
23
|
quotes_table: QuotesTable,
|
|
24
|
+
account_summary: AccountSummary,
|
|
24
25
|
):
|
|
25
26
|
"""
|
|
26
27
|
Initialize the event handler.
|
|
@@ -28,13 +29,15 @@ class KernelEventHandler:
|
|
|
28
29
|
Args:
|
|
29
30
|
output: REPL output widget for displaying text
|
|
30
31
|
positions_table: Positions table widget for displaying positions data
|
|
31
|
-
orders_table: Orders table widget for displaying orders data
|
|
32
|
-
quotes_table: Quotes table widget for displaying market quotes
|
|
32
|
+
orders_table: Orders table widget for displaying orders data
|
|
33
|
+
quotes_table: Quotes table widget for displaying market quotes
|
|
34
|
+
account_summary: Account summary bar widget
|
|
33
35
|
"""
|
|
34
36
|
self.output = output
|
|
35
37
|
self.positions_table = positions_table
|
|
36
38
|
self.orders_table = orders_table
|
|
37
39
|
self.quotes_table = quotes_table
|
|
40
|
+
self.account_summary = account_summary
|
|
38
41
|
self._dashboard_busy = False
|
|
39
42
|
self._on_dashboard_update: Callable[[], None] | None = None
|
|
40
43
|
|
|
@@ -60,9 +63,11 @@ class KernelEventHandler:
|
|
|
60
63
|
positions = payload.get("positions", [])
|
|
61
64
|
orders = payload.get("orders", [])
|
|
62
65
|
quotes = payload.get("quotes", {})
|
|
66
|
+
account_summary = payload.get("account_summary", [])
|
|
63
67
|
self.positions_table.update_positions(positions)
|
|
64
68
|
self.orders_table.update_orders(orders)
|
|
65
69
|
self.quotes_table.update_quotes(quotes)
|
|
70
|
+
self.account_summary.update_summary(account_summary)
|
|
66
71
|
except Exception as e:
|
|
67
72
|
logger.error(f"Dashboard update error: {e}")
|
|
68
73
|
self.output.write(f"Dashboard update failed: {e}")
|
|
@@ -210,6 +210,21 @@ def _quotes_as_records():
|
|
|
210
210
|
pass # Context not ready yet
|
|
211
211
|
return quotes
|
|
212
212
|
|
|
213
|
+
def _account_summary():
|
|
214
|
+
\"\"\"Collect per-exchange account summary (total capital, leverage).\"\"\"
|
|
215
|
+
summary = []
|
|
216
|
+
try:
|
|
217
|
+
for exch in ctx.exchanges:
|
|
218
|
+
summary.append({{
|
|
219
|
+
"exchange": exch,
|
|
220
|
+
"total_capital": _sanitize_number(round(ctx.get_total_capital(exch), 2)),
|
|
221
|
+
"net_leverage": _sanitize_number(round(ctx.get_net_leverage(exch), 4)),
|
|
222
|
+
"gross_leverage": _sanitize_number(round(ctx.get_gross_leverage(exch), 4)),
|
|
223
|
+
}})
|
|
224
|
+
except Exception:
|
|
225
|
+
pass
|
|
226
|
+
return summary
|
|
227
|
+
|
|
213
228
|
def emit_dashboard(all=True, debug=False):
|
|
214
229
|
\"\"\"Publish unified dashboard data via custom MIME for Textual to capture.\"\"\"
|
|
215
230
|
try:
|
|
@@ -217,6 +232,7 @@ def emit_dashboard(all=True, debug=False):
|
|
|
217
232
|
"positions": _positions_as_records(all=all),
|
|
218
233
|
"orders": _orders_as_records(),
|
|
219
234
|
"quotes": _quotes_as_records(),
|
|
235
|
+
"account_summary": _account_summary(),
|
|
220
236
|
}}
|
|
221
237
|
|
|
222
238
|
# Let strategy inject custom data
|
|
@@ -19,6 +19,21 @@ Header {
|
|
|
19
19
|
text-style: bold;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
#account-summary {
|
|
23
|
+
height: auto;
|
|
24
|
+
background: $panel;
|
|
25
|
+
padding: 0 2;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#account-summary-bar {
|
|
29
|
+
height: auto;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#account-summary-content {
|
|
33
|
+
height: auto;
|
|
34
|
+
color: $foreground;
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
Footer {
|
|
23
38
|
dock: bottom;
|
|
24
39
|
height: 1;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Textual UI widgets for strategy runner."""
|
|
2
2
|
|
|
3
|
+
from .account_summary import AccountSummary
|
|
3
4
|
from .command_input import CommandInput
|
|
4
5
|
from .debug_log import DebugLog, TextualLogHandler
|
|
5
6
|
from .orders_table import OrdersTable
|
|
@@ -7,4 +8,4 @@ from .positions_table import PositionsTable
|
|
|
7
8
|
from .quotes_table import QuotesTable
|
|
8
9
|
from .repl_output import ReplOutput
|
|
9
10
|
|
|
10
|
-
__all__ = ["ReplOutput", "PositionsTable", "OrdersTable", "QuotesTable", "CommandInput", "DebugLog", "TextualLogHandler"]
|
|
11
|
+
__all__ = ["AccountSummary", "ReplOutput", "PositionsTable", "OrdersTable", "QuotesTable", "CommandInput", "DebugLog", "TextualLogHandler"]
|