Qubx 1.0.6.dev6__tar.gz → 1.0.6.dev7__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.0.6.dev6 → qubx-1.0.6.dev7}/PKG-INFO +1 -1
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/_version.py +2 -2
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/data.py +3 -8
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/management.py +6 -5
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/runner.py +1 -1
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/cli/commands.py +101 -4
- qubx-1.0.6.dev7/src/qubx/cli/s3.py +124 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/cli/tui.py +85 -3
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/config.py +1 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/interfaces.py +5 -1
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/loggers.py +1 -1
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/metrics.py +80 -19
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/mixins/subscription.py +5 -1
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/cache.py +74 -47
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/guards.py +45 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/trackers/sizers.py +6 -2
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/results.py +10 -116
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/runner.py +78 -75
- qubx-1.0.6.dev7/src/qubx/utils/s3.py +310 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/.gitignore +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/LICENSE +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/README.md +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/pyproject.toml +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/scripts/build.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/_nb_magic.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/account.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/broker.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/iteratedstream.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/ome.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/optimization.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/simulator.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/transfers.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/utils.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/cli/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/cli/deploy.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/cli/misc.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/cli/release.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/cli/resolver.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/cli/theme.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/okx/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/okx/account.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/okx/broker.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/okx/okx.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/registry.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/account.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/basics.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/context.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/detectors/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/detectors/delisting.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/detectors/stale.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/errors.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/exceptions.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/helpers.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/initializer.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/lookups.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/mixins/market.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/mixins/processing.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/mixins/utils.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/series.pxd +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/series.pyi +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/series.pyx +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/utils.pyi +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/utils.pyx +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/containers.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/registry.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/storage.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/storages/ccxt.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/storages/csv.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/storages/handy.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/storages/multi.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/storages/questdb.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/storages/stub.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/storages/utils.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/transformers.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/emitters/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/emitters/base.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/emitters/composite.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/emitters/csv.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/emitters/indicator.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/emitters/questdb.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/exporters/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/exporters/composite.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/exporters/formatters/target_position.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/exporters/slack.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/gathering/simplest.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/health/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/health/base.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/health/dummy.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/health/server.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/loggers/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/loggers/csv.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/loggers/factory.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/loggers/mongo.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/notifications/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/notifications/composite.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/notifications/slack.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/notifications/throttler.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/pandaz/stats.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/pandaz/ta.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/pandaz/utils.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/plugins/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/plugins/loader.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/_build.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restarts/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restorers/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restorers/balance.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restorers/factory.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restorers/position.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restorers/signal.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restorers/state.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restorers/utils.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/state/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/state/dummy.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/state/redis.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/ta/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/base.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/project/template.yml +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/trackers/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/trackers/advanced.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/trackers/composite.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/charting/orderbook.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/collections.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/hft/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/hft/numba_utils.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/hft/orderbook.pyi +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/hft/orderbook.pyx +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/misc.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/nonce.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/ntp.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/orderbook.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/questdb.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/rate_limiter.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/ringbuffer.pxd +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/ringbuffer.pyi +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/ringbuffer.pyx +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/configs.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/kernel_service.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/app.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/handlers.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/init_code.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/kernel.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/styles.tcss +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/slack.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/throttler.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/time.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/websocket_manager.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
- {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/tests/strategies/obi_trader/src/obi_trader/models/obi_trader.py +0 -0
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.0.6.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 0, 6, '
|
|
31
|
+
__version__ = version = '1.0.6.dev7'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 0, 6, 'dev7')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -102,19 +102,14 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
102
102
|
# - try to peek most recent market data
|
|
103
103
|
h_data = self._data_source.peek_historical_data(i, subscription_type)
|
|
104
104
|
if h_data:
|
|
105
|
-
|
|
105
|
+
self.channel.send((i, subscription_type, h_data, True))
|
|
106
|
+
|
|
106
107
|
last_update = h_data[-1]
|
|
107
108
|
if last_quote := self._account._exchange.emulate_quote_from_data(i, last_update.time, last_update): # type: ignore
|
|
108
|
-
# - send historical data to the channel
|
|
109
|
-
self.channel.send((i, subscription_type, h_data, True))
|
|
110
|
-
|
|
111
|
-
# - set last quote
|
|
112
109
|
self._last_quotes[i] = last_quote
|
|
113
|
-
|
|
114
|
-
# - also need to pass this quote to OME !
|
|
115
110
|
self._account.process_market_data(last_quote.time, i, last_quote) # type: ignore
|
|
116
111
|
|
|
117
|
-
|
|
112
|
+
logger.debug(f" | subscribed {subscription_type} {i} -> {len(h_data)} records")
|
|
118
113
|
|
|
119
114
|
def unsubscribe(self, subscription_type: str, instruments: set[Instrument] | Instrument | None = None) -> None:
|
|
120
115
|
# logger.debug(f" | unsubscribe: {subscription_type} -> {instruments}")
|
|
@@ -49,7 +49,8 @@ import pandas as pd
|
|
|
49
49
|
|
|
50
50
|
from qubx.core.metrics import TradingSessionResult
|
|
51
51
|
from qubx.utils.misc import blue, cyan, green, magenta, red, yellow
|
|
52
|
-
from qubx.utils.results import SimulationResultsSaver
|
|
52
|
+
from qubx.utils.results import SimulationResultsSaver
|
|
53
|
+
from qubx.utils.s3 import S3Client, is_cloud_path
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
class BacktestStorage:
|
|
@@ -119,8 +120,10 @@ class BacktestStorage:
|
|
|
119
120
|
self.base_path = base_path.rstrip("/") + "/"
|
|
120
121
|
self._is_cloud = is_cloud_path(base_path)
|
|
121
122
|
|
|
122
|
-
# - for cloud paths: resolve credentials once
|
|
123
|
-
self._storage_options: dict | None =
|
|
123
|
+
# - for cloud paths: resolve credentials once
|
|
124
|
+
self._storage_options: dict | None = (
|
|
125
|
+
S3Client(storage_options=storage_options).storage_options if self._is_cloud else None
|
|
126
|
+
)
|
|
124
127
|
self._conn = self._duckdb.connect()
|
|
125
128
|
|
|
126
129
|
if self._is_cloud:
|
|
@@ -130,14 +133,12 @@ class BacktestStorage:
|
|
|
130
133
|
"""Configure DuckDB httpfs extension for cloud storage access."""
|
|
131
134
|
self._conn.execute("INSTALL httpfs; LOAD httpfs;")
|
|
132
135
|
|
|
133
|
-
# - _storage_options is already resolved at __init__ for cloud paths
|
|
134
136
|
opts = self._storage_options or {}
|
|
135
137
|
if "key" in opts:
|
|
136
138
|
self._conn.execute(f"SET s3_access_key_id='{opts['key']}';")
|
|
137
139
|
if "secret" in opts:
|
|
138
140
|
self._conn.execute(f"SET s3_secret_access_key='{opts['secret']}';")
|
|
139
141
|
if "endpoint_url" in opts:
|
|
140
|
-
# - strip protocol prefix — DuckDB expects hostname only
|
|
141
142
|
endpoint = opts["endpoint_url"].removeprefix("https://").removeprefix("http://")
|
|
142
143
|
self._conn.execute(f"SET s3_endpoint='{endpoint}';")
|
|
143
144
|
if "client_kwargs" in opts:
|
|
@@ -437,7 +437,7 @@ class SimulationRunner:
|
|
|
437
437
|
# - TimeGuard is applied by callers that need it (aux/strategy-facing access).
|
|
438
438
|
# - Main simulation data (DataPump) must NOT be time-guarded: DataPump provides
|
|
439
439
|
# its own explicit [start, end] window and needs to read the full range upfront.
|
|
440
|
-
_prefetch_period = str(
|
|
440
|
+
_prefetch_period = str(min(self.stop - self.start, pd.Timedelta(prefetch_cfg.prefetch_period)))
|
|
441
441
|
return CachedStorage(
|
|
442
442
|
storage, prefetch_period=_prefetch_period, cache_factory=lambda: MemoryCache(prefetch_cfg.cache_size_mb)
|
|
443
443
|
)
|
|
@@ -248,6 +248,13 @@ def run(
|
|
|
248
248
|
"Overrides the 'name:' field from the config file.",
|
|
249
249
|
show_default=True,
|
|
250
250
|
)
|
|
251
|
+
@click.option(
|
|
252
|
+
"--log-file",
|
|
253
|
+
default=None,
|
|
254
|
+
type=click.Path(dir_okay=False),
|
|
255
|
+
help="Write simulation logs to a specific file path.",
|
|
256
|
+
show_default=True,
|
|
257
|
+
)
|
|
251
258
|
def simulate(
|
|
252
259
|
config_file: Path,
|
|
253
260
|
start: str | None,
|
|
@@ -256,6 +263,7 @@ def simulate(
|
|
|
256
263
|
report: str | None,
|
|
257
264
|
log_to_file: bool,
|
|
258
265
|
name: str | None,
|
|
266
|
+
log_file: str | None,
|
|
259
267
|
):
|
|
260
268
|
"""
|
|
261
269
|
Simulates the strategy with the given configuration file.
|
|
@@ -267,7 +275,9 @@ def simulate(
|
|
|
267
275
|
add_project_to_system_path(str(config_file.parent))
|
|
268
276
|
logo()
|
|
269
277
|
logger.info(f"Process PID: <g>{os.getpid()}</g>")
|
|
270
|
-
simulate_strategy(
|
|
278
|
+
simulate_strategy(
|
|
279
|
+
config_file, output, start, end, report, log_to_file, name=name, log_file=log_file,
|
|
280
|
+
)
|
|
271
281
|
|
|
272
282
|
|
|
273
283
|
@main.command()
|
|
@@ -467,9 +477,10 @@ def deploy(zip_file: str, output_dir: str | None, force: bool, system: bool):
|
|
|
467
477
|
@click.argument(
|
|
468
478
|
"results-path",
|
|
469
479
|
type=str,
|
|
470
|
-
default=
|
|
480
|
+
default=None,
|
|
481
|
+
required=False,
|
|
471
482
|
)
|
|
472
|
-
def browse(results_path: str):
|
|
483
|
+
def browse(results_path: str | None):
|
|
473
484
|
"""
|
|
474
485
|
Browse backtest results using an interactive TUI.
|
|
475
486
|
|
|
@@ -487,10 +498,14 @@ def browse(results_path: str):
|
|
|
487
498
|
|
|
488
499
|
qubx browse s3://my-bucket/backtests/
|
|
489
500
|
"""
|
|
490
|
-
from qubx.
|
|
501
|
+
from qubx.config import get_settings
|
|
502
|
+
from qubx.utils.s3 import is_cloud_path
|
|
491
503
|
|
|
492
504
|
from .tui import run_backtest_browser
|
|
493
505
|
|
|
506
|
+
if results_path is None:
|
|
507
|
+
results_path = get_settings().default_browse_path or "results"
|
|
508
|
+
|
|
494
509
|
# - only resolve local paths; cloud URIs (s3://, gs://, az://) are passed as-is
|
|
495
510
|
if not is_cloud_path(results_path):
|
|
496
511
|
results_path = os.path.abspath(os.path.expanduser(results_path))
|
|
@@ -755,5 +770,87 @@ def backtests(storage_path: str, where: str | None, order_by: str, limit: int |
|
|
|
755
770
|
bs.print(where=where, order_by=order_by, limit=limit, params=params)
|
|
756
771
|
|
|
757
772
|
|
|
773
|
+
@main.group()
|
|
774
|
+
def s3():
|
|
775
|
+
"""
|
|
776
|
+
Operate on S3-compatible storage using named accounts.
|
|
777
|
+
|
|
778
|
+
Paths use the format: account:bucket/path
|
|
779
|
+
|
|
780
|
+
Examples:\n
|
|
781
|
+
qubx s3 ls hetzner:frab/spreads/\n
|
|
782
|
+
qubx s3 rm hetzner:frab/spreads/ --recursive\n
|
|
783
|
+
qubx s3 cp hetzner:frab/spreads/data.parquet ./local_copy.parquet
|
|
784
|
+
"""
|
|
785
|
+
pass
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
@s3.command("accounts")
|
|
789
|
+
def s3_accounts_cmd():
|
|
790
|
+
"""List configured S3 accounts."""
|
|
791
|
+
from .s3 import s3_accounts
|
|
792
|
+
|
|
793
|
+
s3_accounts()
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
@s3.command("ls")
|
|
797
|
+
@click.argument("path", type=str)
|
|
798
|
+
@click.option("--recursive", "-r", is_flag=True, help="List files recursively.")
|
|
799
|
+
@click.option("--long", "-l", is_flag=True, help="Show detailed info (size, type).")
|
|
800
|
+
def s3_ls_cmd(path: str, recursive: bool, long: bool):
|
|
801
|
+
"""
|
|
802
|
+
List files in an S3 path.
|
|
803
|
+
|
|
804
|
+
Examples:\n
|
|
805
|
+
qubx s3 ls hetzner:frab/\n
|
|
806
|
+
qubx s3 ls hetzner:frab/spreads/ -r -l
|
|
807
|
+
"""
|
|
808
|
+
from .s3 import s3_ls
|
|
809
|
+
|
|
810
|
+
s3_ls(path, recursive=recursive, long=long)
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
@s3.command("rm")
|
|
814
|
+
@click.argument("path", type=str)
|
|
815
|
+
@click.option("--recursive", "-r", is_flag=True, help="Delete recursively.")
|
|
816
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt.")
|
|
817
|
+
def s3_rm_cmd(path: str, recursive: bool, yes: bool):
|
|
818
|
+
"""
|
|
819
|
+
Delete files from S3.
|
|
820
|
+
|
|
821
|
+
Examples:\n
|
|
822
|
+
qubx s3 rm hetzner:frab/spreads/long_exchange=BINANCE.UM/asset=BTC/data.parquet\n
|
|
823
|
+
qubx s3 rm hetzner:frab/spreads/ -r
|
|
824
|
+
"""
|
|
825
|
+
if not yes:
|
|
826
|
+
label = f"{path} (recursive)" if recursive else path
|
|
827
|
+
click.confirm(f"Delete {label}?", abort=True)
|
|
828
|
+
|
|
829
|
+
from .s3 import s3_rm
|
|
830
|
+
|
|
831
|
+
s3_rm(path, recursive=recursive)
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
@s3.command("cp")
|
|
835
|
+
@click.argument("src", type=str)
|
|
836
|
+
@click.argument("dst", type=str)
|
|
837
|
+
@click.option("--recursive", "-r", is_flag=True, help="Copy recursively.")
|
|
838
|
+
def s3_cp_cmd(src: str, dst: str, recursive: bool):
|
|
839
|
+
"""
|
|
840
|
+
Copy files to/from S3.
|
|
841
|
+
|
|
842
|
+
At least one of SRC or DST must be an S3 path (account:bucket/path).
|
|
843
|
+
The other can be a local path for upload/download.
|
|
844
|
+
|
|
845
|
+
Examples:\n
|
|
846
|
+
qubx s3 cp hetzner:frab/data.parquet ./local.parquet\n
|
|
847
|
+
qubx s3 cp ./local.parquet hetzner:frab/data.parquet\n
|
|
848
|
+
qubx s3 cp hetzner:frab/spreads/ ./spreads/ -r
|
|
849
|
+
"""
|
|
850
|
+
from .s3 import s3_cp
|
|
851
|
+
|
|
852
|
+
s3_cp(src, dst, recursive=recursive)
|
|
853
|
+
|
|
854
|
+
|
|
758
855
|
if __name__ == "__main__":
|
|
759
856
|
main()
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""S3 storage operations using named accounts from Qubx settings.
|
|
2
|
+
|
|
3
|
+
Uses :class:`qubx.utils.s3.S3Client` for all operations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from pyarrow.fs import FileType, LocalFileSystem, copy_files
|
|
8
|
+
|
|
9
|
+
from qubx.config import get_settings
|
|
10
|
+
from qubx.utils.s3 import S3Client, parse_uri
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _human_size(nbytes: int) -> str:
|
|
14
|
+
for unit in ("B", "KB", "MB", "GB", "TB"):
|
|
15
|
+
if abs(nbytes) < 1024:
|
|
16
|
+
return f"{nbytes:6.1f} {unit}"
|
|
17
|
+
nbytes /= 1024 # type: ignore[assignment]
|
|
18
|
+
return f"{nbytes:.1f} PB"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def s3_ls(path: str, recursive: bool = False, long: bool = False) -> None:
|
|
22
|
+
try:
|
|
23
|
+
client, s3_path = S3Client.from_uri(path)
|
|
24
|
+
except ValueError as e:
|
|
25
|
+
raise click.BadParameter(str(e))
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
entries = client.ls(s3_path, recursive=recursive)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
click.echo(click.style(f"Error listing {s3_path}: {e}", fg="red"), err=True)
|
|
31
|
+
raise click.Abort()
|
|
32
|
+
|
|
33
|
+
if not entries:
|
|
34
|
+
click.echo(click.style(f"No files found under: {s3_path}", fg="yellow"))
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
for info in sorted(entries, key=lambda e: e.path):
|
|
38
|
+
if long:
|
|
39
|
+
size = _human_size(info.size) if info.type == FileType.File else " -"
|
|
40
|
+
kind = "DIR " if info.type == FileType.Directory else "FILE"
|
|
41
|
+
click.echo(f" {kind} {size} {info.path}")
|
|
42
|
+
else:
|
|
43
|
+
click.echo(f" {info.path}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def s3_rm(path: str, recursive: bool = False) -> None:
|
|
47
|
+
try:
|
|
48
|
+
client, s3_path = S3Client.from_uri(path)
|
|
49
|
+
except ValueError as e:
|
|
50
|
+
raise click.BadParameter(str(e))
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
if recursive:
|
|
54
|
+
entries = client.ls(s3_path, recursive=True)
|
|
55
|
+
files = [e for e in entries if e.type == FileType.File]
|
|
56
|
+
if not files:
|
|
57
|
+
click.echo(click.style(f"No files found under: {s3_path}", fg="yellow"))
|
|
58
|
+
return
|
|
59
|
+
click.echo(f"Deleting {len(files)} file(s) under {s3_path} ...")
|
|
60
|
+
client.rm(s3_path, recursive=recursive)
|
|
61
|
+
click.echo(click.style(f"✓ Deleted {s3_path}", fg="green"))
|
|
62
|
+
except Exception as e:
|
|
63
|
+
click.echo(click.style(f"Error deleting {s3_path}: {e}", fg="red"), err=True)
|
|
64
|
+
raise click.Abort()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def s3_cp(src: str, dst: str, recursive: bool = False) -> None:
|
|
68
|
+
src_is_s3 = ":" in src
|
|
69
|
+
dst_is_s3 = ":" in dst
|
|
70
|
+
|
|
71
|
+
if src_is_s3 and dst_is_s3:
|
|
72
|
+
try:
|
|
73
|
+
src_account, src_path = parse_uri(src)
|
|
74
|
+
dst_account, dst_path = parse_uri(dst)
|
|
75
|
+
except ValueError as e:
|
|
76
|
+
raise click.BadParameter(str(e))
|
|
77
|
+
|
|
78
|
+
if src_account != dst_account:
|
|
79
|
+
click.echo(
|
|
80
|
+
click.style("Cross-account copy is not supported yet.", fg="red"),
|
|
81
|
+
err=True,
|
|
82
|
+
)
|
|
83
|
+
raise click.Abort()
|
|
84
|
+
client = S3Client(account=src_account)
|
|
85
|
+
client.copy_s3(src_path, dst_path, recursive=recursive)
|
|
86
|
+
click.echo(click.style(f"✓ Copied {src_path} → {dst_path}", fg="green"))
|
|
87
|
+
|
|
88
|
+
elif src_is_s3 and not dst_is_s3:
|
|
89
|
+
try:
|
|
90
|
+
client, src_path = S3Client.from_uri(src)
|
|
91
|
+
except ValueError as e:
|
|
92
|
+
raise click.BadParameter(str(e))
|
|
93
|
+
copy_files(src_path, dst, source_filesystem=client.fs, destination_filesystem=LocalFileSystem())
|
|
94
|
+
click.echo(click.style(f"✓ Downloaded {src_path} → {dst}", fg="green"))
|
|
95
|
+
|
|
96
|
+
elif not src_is_s3 and dst_is_s3:
|
|
97
|
+
try:
|
|
98
|
+
client, dst_path = S3Client.from_uri(dst)
|
|
99
|
+
except ValueError as e:
|
|
100
|
+
raise click.BadParameter(str(e))
|
|
101
|
+
copy_files(src, dst_path, source_filesystem=LocalFileSystem(), destination_filesystem=client.fs)
|
|
102
|
+
click.echo(click.style(f"✓ Uploaded {src} → {dst_path}", fg="green"))
|
|
103
|
+
|
|
104
|
+
else:
|
|
105
|
+
click.echo(
|
|
106
|
+
click.style("At least one path must be an S3 path (account:bucket/path).", fg="red"),
|
|
107
|
+
err=True,
|
|
108
|
+
)
|
|
109
|
+
raise click.Abort()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def s3_accounts() -> None:
|
|
113
|
+
s = get_settings()
|
|
114
|
+
if not s.s3:
|
|
115
|
+
click.echo("No S3 accounts configured.")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
click.echo(click.style("Configured S3 accounts:", fg="cyan", bold=True))
|
|
119
|
+
for name, acct in s.s3.items():
|
|
120
|
+
default_marker = " (default)" if name == s.default_s3_account else ""
|
|
121
|
+
click.echo(f" {click.style(name, fg='yellow')}{default_marker}")
|
|
122
|
+
click.echo(f" endpoint: {acct.endpoint_url}")
|
|
123
|
+
if acct.region:
|
|
124
|
+
click.echo(f" region: {acct.region}")
|
|
@@ -10,6 +10,8 @@ from textual import on, work
|
|
|
10
10
|
from textual.app import App, ComposeResult
|
|
11
11
|
from textual.binding import Binding
|
|
12
12
|
from textual.containers import Horizontal, Vertical
|
|
13
|
+
from textual.coordinate import Coordinate
|
|
14
|
+
from textual.timer import Timer
|
|
13
15
|
from textual.widgets import (
|
|
14
16
|
Button,
|
|
15
17
|
DataTable,
|
|
@@ -699,6 +701,7 @@ class BacktestBrowserApp(App):
|
|
|
699
701
|
DARK = True
|
|
700
702
|
|
|
701
703
|
BINDINGS = [
|
|
704
|
+
Binding("q", "quit", "Quit", show=True),
|
|
702
705
|
Binding("j", "vim_down", "Down", show=False),
|
|
703
706
|
Binding("k", "vim_up", "Up", show=False),
|
|
704
707
|
Binding("g", "vim_home", "Top", show=False),
|
|
@@ -707,6 +710,7 @@ class BacktestBrowserApp(App):
|
|
|
707
710
|
Binding("S", "sort_by_key_sharpe", "Sort Sharpe", show=True),
|
|
708
711
|
Binding("C", "sort_by_key_cagr", "Sort CAGR", show=True),
|
|
709
712
|
Binding("G", "vim_end", "Sort Gain", show=True), # - Tree: scroll end; Table: sort gain
|
|
713
|
+
Binding("T", "sort_by_key_created", "Sort Created", show=True),
|
|
710
714
|
]
|
|
711
715
|
|
|
712
716
|
CSS = """
|
|
@@ -865,6 +869,9 @@ class BacktestBrowserApp(App):
|
|
|
865
869
|
self._selected_result: dict[str, Any] | None = None
|
|
866
870
|
# - tracks which node is currently being loaded to discard stale callbacks
|
|
867
871
|
self._loading_node: BacktestTreeNode | None = None
|
|
872
|
+
# - cc (copy path) state machine
|
|
873
|
+
self._pending_copy: bool = False
|
|
874
|
+
self._pending_copy_timer: Timer | None = None
|
|
868
875
|
|
|
869
876
|
def compose(self) -> ComposeResult:
|
|
870
877
|
"""Compose the TUI layout"""
|
|
@@ -880,6 +887,7 @@ class BacktestBrowserApp(App):
|
|
|
880
887
|
yield Button("Sort by Sharpe", id="sort-sharpe")
|
|
881
888
|
yield Button("Sort by CAGR", id="sort-cagr")
|
|
882
889
|
yield Button("Sort by Gain", id="sort-gain")
|
|
890
|
+
yield Button("Sort by Created", id="sort-created")
|
|
883
891
|
yield Button("Open HTML", id="open-html")
|
|
884
892
|
yield Button("Refresh", id="refresh")
|
|
885
893
|
|
|
@@ -910,7 +918,7 @@ class BacktestBrowserApp(App):
|
|
|
910
918
|
if node_data.backtest_results:
|
|
911
919
|
# - already loaded: show immediately (cached after first click)
|
|
912
920
|
self.current_results = node_data.backtest_results
|
|
913
|
-
self.
|
|
921
|
+
self._sort_table("creation_time")
|
|
914
922
|
self.query_one("#metrics-table", MetricsTable).focus()
|
|
915
923
|
|
|
916
924
|
elif node_data.pending_ids:
|
|
@@ -1011,7 +1019,7 @@ class BacktestBrowserApp(App):
|
|
|
1011
1019
|
|
|
1012
1020
|
self.current_results = node_data.backtest_results
|
|
1013
1021
|
if self.current_results:
|
|
1014
|
-
self.
|
|
1022
|
+
self._sort_table("creation_time")
|
|
1015
1023
|
table.focus()
|
|
1016
1024
|
else:
|
|
1017
1025
|
self.call_later(
|
|
@@ -1034,6 +1042,11 @@ class BacktestBrowserApp(App):
|
|
|
1034
1042
|
"""Sort results by total gain"""
|
|
1035
1043
|
self._sort_table("gain")
|
|
1036
1044
|
|
|
1045
|
+
@on(Button.Pressed, "#sort-created")
|
|
1046
|
+
def sort_by_created(self) -> None:
|
|
1047
|
+
"""Sort results by creation time"""
|
|
1048
|
+
self._sort_table("creation_time")
|
|
1049
|
+
|
|
1037
1050
|
@on(Button.Pressed, "#open-html")
|
|
1038
1051
|
def open_html_tearsheet(self) -> None:
|
|
1039
1052
|
"""Open full HTML tearsheet in browser for the selected result"""
|
|
@@ -1071,7 +1084,14 @@ class BacktestBrowserApp(App):
|
|
|
1071
1084
|
``_fv`` substitutes 0.0 for NaN / None so those rows sink to the bottom.
|
|
1072
1085
|
"""
|
|
1073
1086
|
if self.current_results:
|
|
1074
|
-
|
|
1087
|
+
if metric == "creation_time":
|
|
1088
|
+
# - timestamps cannot go through _fv (float coercion); use pd.Timestamp
|
|
1089
|
+
self.current_results.sort(
|
|
1090
|
+
key=lambda x: pd.Timestamp(x.get("creation_time") or "1970-01-01"),
|
|
1091
|
+
reverse=True,
|
|
1092
|
+
)
|
|
1093
|
+
else:
|
|
1094
|
+
self.current_results.sort(key=lambda x: _fv(x.get(metric)), reverse=True)
|
|
1075
1095
|
self._update_table()
|
|
1076
1096
|
|
|
1077
1097
|
def _update_table(self) -> None:
|
|
@@ -1163,6 +1183,68 @@ class BacktestBrowserApp(App):
|
|
|
1163
1183
|
if not isinstance(self.focused, Tree):
|
|
1164
1184
|
self._sort_table("cagr")
|
|
1165
1185
|
|
|
1186
|
+
def action_sort_by_key_created(self) -> None:
|
|
1187
|
+
"""Sort table by creation time (T — right pane only)."""
|
|
1188
|
+
if not isinstance(self.focused, Tree):
|
|
1189
|
+
self._sort_table("creation_time")
|
|
1190
|
+
|
|
1191
|
+
# ── cc copy path ─────────────────────────────────────────────────────────
|
|
1192
|
+
|
|
1193
|
+
def on_key(self, event) -> None:
|
|
1194
|
+
"""Handle key events for multi-key bindings (cc = copy path to clipboard)."""
|
|
1195
|
+
if event.key == "c":
|
|
1196
|
+
if not isinstance(self.focused, DataTable):
|
|
1197
|
+
# - only intercept c when DataTable is focused
|
|
1198
|
+
return
|
|
1199
|
+
if self._pending_copy:
|
|
1200
|
+
# - second 'c' within timeout: complete the copy
|
|
1201
|
+
self._pending_copy = False
|
|
1202
|
+
if self._pending_copy_timer:
|
|
1203
|
+
self._pending_copy_timer.stop()
|
|
1204
|
+
self._pending_copy_timer = None
|
|
1205
|
+
self._copy_current_path()
|
|
1206
|
+
event.prevent_default()
|
|
1207
|
+
event.stop()
|
|
1208
|
+
else:
|
|
1209
|
+
# - first 'c': enter pending state, start timeout
|
|
1210
|
+
self._pending_copy = True
|
|
1211
|
+
self._pending_copy_timer = self.set_timer(
|
|
1212
|
+
0.5, self._cancel_pending_copy, name="cc_timeout"
|
|
1213
|
+
)
|
|
1214
|
+
event.prevent_default()
|
|
1215
|
+
event.stop()
|
|
1216
|
+
elif self._pending_copy:
|
|
1217
|
+
# - any other key cancels the pending copy
|
|
1218
|
+
self._cancel_pending_copy()
|
|
1219
|
+
|
|
1220
|
+
def _cancel_pending_copy(self) -> None:
|
|
1221
|
+
"""Cancel the pending cc copy operation (timeout or different key)."""
|
|
1222
|
+
self._pending_copy = False
|
|
1223
|
+
if self._pending_copy_timer:
|
|
1224
|
+
self._pending_copy_timer.stop()
|
|
1225
|
+
self._pending_copy_timer = None
|
|
1226
|
+
|
|
1227
|
+
def _copy_current_path(self) -> None:
|
|
1228
|
+
"""Copy the path of the currently highlighted table row to clipboard via OSC 52."""
|
|
1229
|
+
table = self.query_one("#metrics-table", MetricsTable)
|
|
1230
|
+
if table.row_count == 0:
|
|
1231
|
+
return
|
|
1232
|
+
|
|
1233
|
+
try:
|
|
1234
|
+
cursor_row = table.cursor_row
|
|
1235
|
+
cell_key = table.coordinate_to_cell_key(Coordinate(cursor_row, 0))
|
|
1236
|
+
load_id = cell_key.row_key.value
|
|
1237
|
+
except Exception:
|
|
1238
|
+
self.notify("No row selected", severity="warning", timeout=2)
|
|
1239
|
+
return
|
|
1240
|
+
|
|
1241
|
+
if not load_id:
|
|
1242
|
+
return
|
|
1243
|
+
|
|
1244
|
+
# - load_id is the backtest_id = relative path from storage root
|
|
1245
|
+
self.copy_to_clipboard(str(load_id))
|
|
1246
|
+
self.notify(f"Copied: {load_id}", timeout=3)
|
|
1247
|
+
|
|
1166
1248
|
# ── tearsheet ────────────────────────────────────────────────────────────
|
|
1167
1249
|
|
|
1168
1250
|
def _generate_html_tearsheet(self, result_info: dict[str, Any]) -> None:
|
|
@@ -93,6 +93,7 @@ class QubxSettings(BaseSettings):
|
|
|
93
93
|
metrics_port: int | None = None
|
|
94
94
|
health_port: int | None = None
|
|
95
95
|
log_format: str = "text" # "text" or "json"
|
|
96
|
+
default_browse_path: str | None = None
|
|
96
97
|
|
|
97
98
|
model_config = {"env_prefix": "QUBX_", "env_nested_delimiter": "__"}
|
|
98
99
|
|
|
@@ -148,7 +148,11 @@ class ITradeDataExport:
|
|
|
148
148
|
pass
|
|
149
149
|
|
|
150
150
|
def export_position_changes(
|
|
151
|
-
self,
|
|
151
|
+
self,
|
|
152
|
+
time: dt_64,
|
|
153
|
+
instrument: Instrument,
|
|
154
|
+
price: float,
|
|
155
|
+
account: "IAccountViewer",
|
|
152
156
|
metadata: dict[str, Any] | None = None,
|
|
153
157
|
) -> None:
|
|
154
158
|
"""
|
|
@@ -137,7 +137,7 @@ class PortfolioLogger(PositionsDumper):
|
|
|
137
137
|
"exchange": i.exchange,
|
|
138
138
|
"market_type": i.market_type,
|
|
139
139
|
"pnl_quoted": p.pnl,
|
|
140
|
-
"quantity": p.quantity,
|
|
140
|
+
"quantity": p.quantity * i.quantity_multiplier,
|
|
141
141
|
"realized_pnl_quoted": p.r_pnl,
|
|
142
142
|
"avg_position_price": p.position_avg_price if p.quantity != 0.0 else 0.0,
|
|
143
143
|
"current_price": p.last_update_price,
|