Qubx 1.0.0.dev2__tar.gz → 1.0.0.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.0.0.dev2 → qubx-1.0.0.dev3}/PKG-INFO +3 -1
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/pyproject.toml +3 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/_version.py +2 -2
- qubx-1.0.0.dev3/src/qubx/backtester/__init__.py +4 -0
- qubx-1.0.0.dev3/src/qubx/backtester/management.py +531 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/backtester/runner.py +57 -18
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/backtester/simulator.py +23 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/cli/commands.py +100 -13
- qubx-1.0.0.dev3/src/qubx/cli/theme.py +61 -0
- qubx-1.0.0.dev3/src/qubx/cli/tui.py +988 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/basics.py +18 -2
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/context.py +7 -1
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/detectors/stale.py +4 -61
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/interfaces.py +13 -7
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/metrics.py +125 -17
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/mixins/market.py +40 -15
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/mixins/processing.py +36 -21
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/data/cache.py +60 -24
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/data/storage.py +6 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/data/storages/ccxt.py +40 -20
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/data/storages/multi.py +10 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/data/storages/questdb.py +4 -1
- qubx-1.0.0.dev3/src/qubx/utils/results.py +997 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/runner/runner.py +107 -39
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/runner/textual/app.py +6 -1
- qubx-1.0.0.dev3/src/qubx/utils/runner/textual/styles.tcss +196 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/time.py +31 -0
- qubx-1.0.0.dev2/src/qubx/backtester/__init__.py +0 -5
- qubx-1.0.0.dev2/src/qubx/backtester/management.py +0 -522
- qubx-1.0.0.dev2/src/qubx/cli/tui.py +0 -458
- qubx-1.0.0.dev2/src/qubx/utils/runner/textual/styles.tcss +0 -134
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/.gitignore +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/LICENSE +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/README.md +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/hatch_build.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/_nb_magic.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/backtester/account.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/backtester/broker.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/backtester/data.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/backtester/iteratedstream.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/backtester/ome.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/backtester/optimization.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/backtester/transfers.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/backtester/utils.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/cli/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/cli/deploy.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/cli/misc.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/cli/release.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/registry.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/account.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/detectors/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/detectors/delisting.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/errors.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/exceptions.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/helpers.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/initializer.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/loggers.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/lookups.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/mixins/utils.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/series.pxd +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/series.pyi +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/series.pyx +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/utils.pyi +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/core/utils.pyx +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/data/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/data/containers.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/data/guards.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/data/registry.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/data/storages/csv.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/data/storages/handy.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/data/storages/stub.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/data/storages/utils.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/data/transformers.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/emitters/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/emitters/base.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/emitters/composite.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/emitters/csv.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/emitters/indicator.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/emitters/questdb.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/exporters/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/exporters/composite.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/exporters/formatters/target_position.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/exporters/slack.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/gathering/simplest.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/health/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/health/base.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/health/dummy.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/loggers/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/loggers/csv.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/loggers/factory.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/loggers/mongo.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/notifications/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/notifications/composite.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/notifications/slack.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/notifications/throttler.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/pandaz/stats.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/pandaz/ta.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/pandaz/utils.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/plugins/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/plugins/loader.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/resources/_build.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/restarts/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/restorers/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/restorers/balance.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/restorers/factory.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/restorers/position.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/restorers/signal.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/restorers/state.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/restorers/utils.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/state/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/state/dummy.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/state/redis.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/ta/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/templates/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/templates/base.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/templates/project/template.yml +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/trackers/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/trackers/advanced.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/trackers/composite.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/trackers/sizers.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/charting/orderbook.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/collections.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/hft/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/hft/numba_utils.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/hft/orderbook.pyi +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/hft/orderbook.pyx +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/misc.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/nonce.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/ntp.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/orderbook.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/questdb.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/rate_limiter.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/ringbuffer.pxd +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/ringbuffer.pyi +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/ringbuffer.pyx +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/runner/configs.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/runner/kernel_service.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/runner/textual/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/runner/textual/handlers.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/runner/textual/init_code.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/runner/textual/kernel.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/slack.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/throttler.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/src/qubx/utils/websocket_manager.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
- {qubx-1.0.0.dev2 → qubx-1.0.0.dev3}/tests/strategies/obi_trader/src/obi_trader/models/obi_trader.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Qubx
|
|
3
|
-
Version: 1.0.0.
|
|
3
|
+
Version: 1.0.0.dev3
|
|
4
4
|
Summary: Qubx - Quantitative Trading Framework
|
|
5
5
|
Project-URL: homepage, https://xlydian.com
|
|
6
6
|
Project-URL: repository, https://github.com/xLydianSoftware/Qubx
|
|
@@ -57,6 +57,8 @@ Requires-Dist: uvloop<1,>=0.22.1; sys_platform != 'win32'
|
|
|
57
57
|
Requires-Dist: websockets==15.0.1
|
|
58
58
|
Provides-Extra: k8
|
|
59
59
|
Requires-Dist: prometheus-client<1,>=0.21.1; extra == 'k8'
|
|
60
|
+
Provides-Extra: storage
|
|
61
|
+
Requires-Dist: duckdb>=1.0.0; extra == 'storage'
|
|
60
62
|
Description-Content-Type: text/markdown
|
|
61
63
|
|
|
62
64
|
# Qubx - Quantitative Trading Framework
|
|
@@ -66,6 +66,8 @@ docs = "https://xlydiansoftware.github.io/Qubx"
|
|
|
66
66
|
[project.optional-dependencies]
|
|
67
67
|
# Runtime optional features only (shipped with package)
|
|
68
68
|
k8 = ["prometheus-client>=0.21.1,<1"]
|
|
69
|
+
# Parquet-based backtest storage with DuckDB search and cloud (S3/GCS/Azure) support
|
|
70
|
+
storage = ["duckdb>=1.0.0"]
|
|
69
71
|
|
|
70
72
|
[project.scripts]
|
|
71
73
|
qubx = "qubx.cli.commands:main"
|
|
@@ -177,4 +179,5 @@ dev = [
|
|
|
177
179
|
"mongomock>=4.3.0,<5",
|
|
178
180
|
"pytest-textual-snapshot>=1.1.0,<2",
|
|
179
181
|
"git-cliff>=2.0.0",
|
|
182
|
+
"duckdb>=1.0.0"
|
|
180
183
|
]
|
|
@@ -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.0.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 0, 0, '
|
|
31
|
+
__version__ = version = '1.0.0.dev3'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 0, 0, 'dev3')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parquet-based backtest storage utilities — schemas, constants, and write helpers.
|
|
3
|
+
|
|
4
|
+
Used by:
|
|
5
|
+
- qubx.core.metrics.TradingSessionResult (result model)
|
|
6
|
+
- qubx.backtester.management.BacktestStorage (query interface)
|
|
7
|
+
- qubx.utils.results.SimulationResultsSaver (save / load)
|
|
8
|
+
- qubx.utils.runner.runner.simulate_strategy (cloud detection, tag helpers)
|
|
9
|
+
|
|
10
|
+
Storage layout (single run)::
|
|
11
|
+
|
|
12
|
+
{base_path}/
|
|
13
|
+
└── {yaml.name}/ # from cfg.name field (required)
|
|
14
|
+
└── {ShortClass}/ # short strategy class name(s), multi joined with '+'
|
|
15
|
+
└── YYYYMMDD_HHMMSS/ # unique per run
|
|
16
|
+
├── _status.parquet # written first, updated live during simulation
|
|
17
|
+
├── _metadata.parquet # written on completion (all perf metrics)
|
|
18
|
+
├── portfolio.parquet
|
|
19
|
+
├── executions.parquet
|
|
20
|
+
├── signals.parquet
|
|
21
|
+
├── targets.parquet
|
|
22
|
+
└── config.yaml # attached config file
|
|
23
|
+
|
|
24
|
+
Storage layout (variation set)::
|
|
25
|
+
|
|
26
|
+
{base_path}/
|
|
27
|
+
└── {yaml.name}/
|
|
28
|
+
└── {ShortClass}/
|
|
29
|
+
└── YYYYMMDD_HHMMSS/
|
|
30
|
+
├── _status.parquet
|
|
31
|
+
├── _metadata.parquet # N rows, one per variation — searchable by DuckDB
|
|
32
|
+
├── var_000/
|
|
33
|
+
│ ├── portfolio.parquet
|
|
34
|
+
│ ├── executions.parquet
|
|
35
|
+
│ ├── signals.parquet
|
|
36
|
+
│ └── targets.parquet
|
|
37
|
+
├── var_001/
|
|
38
|
+
│ └── ...
|
|
39
|
+
└── config.yaml
|
|
40
|
+
|
|
41
|
+
DuckDB examples (via BacktestStorage)::
|
|
42
|
+
|
|
43
|
+
storage.search("sharpe > 2 AND mdd_pct < 25 AND list_contains(tags, 'momentum')")
|
|
44
|
+
storage.status("running")
|
|
45
|
+
storage.get_portfolio("my_strat/Nimble/20240301_120000", symbol="BTCUSDT", start="2024-01-01")
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
import pandas as pd
|
|
49
|
+
|
|
50
|
+
from qubx.core.metrics import TradingSessionResult
|
|
51
|
+
from qubx.utils.misc import blue, cyan, green, magenta, red, yellow
|
|
52
|
+
from qubx.utils.results import SimulationResultsSaver, is_cloud_path, resolve_s3_storage_options
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class BacktestStorage:
|
|
56
|
+
"""
|
|
57
|
+
Query interface for parquet-based backtest storage.
|
|
58
|
+
Supports local directories and cloud paths (S3, GCS, Azure).
|
|
59
|
+
|
|
60
|
+
Uses DuckDB for fast metadata search and data queries across all stored backtests.
|
|
61
|
+
|
|
62
|
+
Storage layout (single run)::
|
|
63
|
+
|
|
64
|
+
{base_path}/
|
|
65
|
+
└── {yaml.name}/ # from cfg.name field (required)
|
|
66
|
+
└── {ShortClass}/ # short strategy class name(s)
|
|
67
|
+
└── YYYYMMDD_HHMMSS/
|
|
68
|
+
├── _status.parquet # live progress, written by SimulationResultsSaver
|
|
69
|
+
├── _metadata.parquet # completion metrics
|
|
70
|
+
├── portfolio.parquet
|
|
71
|
+
├── executions.parquet
|
|
72
|
+
├── signals.parquet
|
|
73
|
+
├── targets.parquet
|
|
74
|
+
├── emitter_data.parquet
|
|
75
|
+
├── transfers.parquet
|
|
76
|
+
└── config.yaml
|
|
77
|
+
|
|
78
|
+
Examples::
|
|
79
|
+
|
|
80
|
+
# - local storage
|
|
81
|
+
storage = BacktestStorage("/backtests/")
|
|
82
|
+
|
|
83
|
+
# - S3 storage (creds from env: QUBX_S3_KEY / AWS_ACCESS_KEY_ID)
|
|
84
|
+
storage = BacktestStorage("s3://my-bucket/backtests/")
|
|
85
|
+
|
|
86
|
+
# - search: full DuckDB SQL WHERE clause
|
|
87
|
+
df = storage.search("sharpe > 2 AND mdd_pct < 25")
|
|
88
|
+
df = storage.search("list_contains(tags, 'momentum') AND cagr > 0.3")
|
|
89
|
+
df = storage.search("json_extract(parameters, '$.fast_period')::int > 10")
|
|
90
|
+
df = storage.search() # - all results
|
|
91
|
+
|
|
92
|
+
# - live status dashboard
|
|
93
|
+
df = storage.status("running")
|
|
94
|
+
|
|
95
|
+
# - load result
|
|
96
|
+
result = storage.load("my_strat/Nimble/20240301_120000")
|
|
97
|
+
|
|
98
|
+
# - best variation from a variation set
|
|
99
|
+
result = storage.load_best_variation("my_strat/Nimble/20240301_130000", by="sharpe")
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, base_path: str, storage_options: dict | None = None):
|
|
103
|
+
"""
|
|
104
|
+
Initialize BacktestStorage.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
base_path: Root path for backtest storage (local dir or cloud URI)
|
|
108
|
+
storage_options: Cloud storage credentials. None = auto-detect from:
|
|
109
|
+
QUBX_S3_KEY / AWS_ACCESS_KEY_ID
|
|
110
|
+
QUBX_S3_SECRET / AWS_SECRET_ACCESS_KEY
|
|
111
|
+
QUBX_S3_REGION / AWS_DEFAULT_REGION
|
|
112
|
+
QUBX_S3_ENDPOINT / AWS_ENDPOINT_URL
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
import duckdb
|
|
116
|
+
|
|
117
|
+
self._duckdb = duckdb
|
|
118
|
+
except ImportError:
|
|
119
|
+
raise ImportError(
|
|
120
|
+
"duckdb is required for BacktestStorage. "
|
|
121
|
+
"Install with: pip install 'qubx[storage]' or pip install duckdb"
|
|
122
|
+
)
|
|
123
|
+
self.base_path = base_path.rstrip("/") + "/"
|
|
124
|
+
self._is_cloud = is_cloud_path(base_path)
|
|
125
|
+
|
|
126
|
+
# - for cloud paths: resolve credentials once (env vars → explicit dict)
|
|
127
|
+
self._storage_options: dict | None = resolve_s3_storage_options(storage_options) if self._is_cloud else None
|
|
128
|
+
self._conn = self._duckdb.connect()
|
|
129
|
+
|
|
130
|
+
if self._is_cloud:
|
|
131
|
+
self._setup_cloud_duckdb()
|
|
132
|
+
|
|
133
|
+
def _setup_cloud_duckdb(self) -> None:
|
|
134
|
+
"""Configure DuckDB httpfs extension for cloud storage access."""
|
|
135
|
+
self._conn.execute("INSTALL httpfs; LOAD httpfs;")
|
|
136
|
+
|
|
137
|
+
# - _storage_options is already resolved at __init__ for cloud paths
|
|
138
|
+
opts = self._storage_options or {}
|
|
139
|
+
if "key" in opts:
|
|
140
|
+
self._conn.execute(f"SET s3_access_key_id='{opts['key']}';")
|
|
141
|
+
if "secret" in opts:
|
|
142
|
+
self._conn.execute(f"SET s3_secret_access_key='{opts['secret']}';")
|
|
143
|
+
if "endpoint_url" in opts:
|
|
144
|
+
# - strip protocol prefix — DuckDB expects hostname only
|
|
145
|
+
endpoint = opts["endpoint_url"].removeprefix("https://").removeprefix("http://")
|
|
146
|
+
self._conn.execute(f"SET s3_endpoint='{endpoint}';")
|
|
147
|
+
if "client_kwargs" in opts:
|
|
148
|
+
region = opts["client_kwargs"].get("region_name")
|
|
149
|
+
if region:
|
|
150
|
+
self._conn.execute(f"SET s3_region='{region}';")
|
|
151
|
+
|
|
152
|
+
def _glob(self, filename: str) -> str:
|
|
153
|
+
"""
|
|
154
|
+
Build recursive glob pattern for a filename within base_path.
|
|
155
|
+
"""
|
|
156
|
+
return f"{self.base_path}**/{filename}"
|
|
157
|
+
|
|
158
|
+
def search(
|
|
159
|
+
self,
|
|
160
|
+
where: str | None = None,
|
|
161
|
+
order_by: str = "sharpe DESC",
|
|
162
|
+
limit: int | None = None,
|
|
163
|
+
) -> pd.DataFrame:
|
|
164
|
+
"""
|
|
165
|
+
Search backtest metadata across all stored results using DuckDB SQL.
|
|
166
|
+
|
|
167
|
+
The WHERE clause has full DuckDB SQL power — no restrictions::
|
|
168
|
+
|
|
169
|
+
"sharpe > 2 AND mdd_pct < 25 AND author = 'alice'"
|
|
170
|
+
"list_contains(tags, 'momentum') AND cagr > 0.3"
|
|
171
|
+
"json_extract(parameters, '$.fast_period')::int > 10"
|
|
172
|
+
"is_variation = false"
|
|
173
|
+
"strategy_class LIKE '%Nimble%'"
|
|
174
|
+
"start >= '2024-01-01' AND sharpe BETWEEN 1.5 AND 4.0"
|
|
175
|
+
|
|
176
|
+
Regular backtests: one row per run.
|
|
177
|
+
Variation sets: N rows per set (one per variation), all with is_variation=true.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
where: DuckDB SQL WHERE clause, or None to return all results
|
|
181
|
+
order_by: ORDER BY clause (default: "sharpe DESC")
|
|
182
|
+
limit: Maximum rows to return
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
pd.DataFrame with matching metadata rows
|
|
186
|
+
"""
|
|
187
|
+
glob = self._glob(SimulationResultsSaver.METADATA_FILE)
|
|
188
|
+
sql = f"SELECT * FROM read_parquet('{glob}', union_by_name=true)"
|
|
189
|
+
if where:
|
|
190
|
+
sql += f" WHERE {where}"
|
|
191
|
+
if order_by:
|
|
192
|
+
sql += f" ORDER BY {order_by}"
|
|
193
|
+
if limit is not None:
|
|
194
|
+
sql += f" LIMIT {limit}"
|
|
195
|
+
return self._conn.execute(sql).df()
|
|
196
|
+
|
|
197
|
+
def status(self, filter_status: str | None = None) -> pd.DataFrame:
|
|
198
|
+
"""
|
|
199
|
+
Get status of all simulations (running, completed, failed, pending).
|
|
200
|
+
|
|
201
|
+
Reads _status.parquet files written by SimulationResultsSaver.
|
|
202
|
+
Works in real-time — running simulations update their status every 1%.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
filter_status: Filter by status value ('running', 'completed', 'failed', 'pending'),
|
|
206
|
+
or None to return all simulations
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
pd.DataFrame with status rows, ordered by started_at DESC
|
|
210
|
+
"""
|
|
211
|
+
glob = self._glob(SimulationResultsSaver.STATUS_FILE)
|
|
212
|
+
sql = f"SELECT * FROM read_parquet('{glob}', union_by_name=true)"
|
|
213
|
+
if filter_status:
|
|
214
|
+
sql += f" WHERE status = '{filter_status}'"
|
|
215
|
+
sql += " ORDER BY started_at DESC"
|
|
216
|
+
return self._conn.execute(sql).df()
|
|
217
|
+
|
|
218
|
+
def _load_from_path(self, run_path: str) -> TradingSessionResult:
|
|
219
|
+
"""
|
|
220
|
+
Load a TradingSessionResult using the already-configured DuckDB connection.
|
|
221
|
+
|
|
222
|
+
All parquet reads go through ``self._conn`` (httpfs for S3, local for disk)
|
|
223
|
+
so no s3fs / aiobotocore dependency is needed and connection pooling is
|
|
224
|
+
handled by DuckDB internally.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
def _read(filename: str) -> pd.DataFrame:
|
|
228
|
+
p = f"{run_path.rstrip('/')}/{filename}"
|
|
229
|
+
try:
|
|
230
|
+
return self._conn.execute(f"SELECT * FROM read_parquet('{p}')").df()
|
|
231
|
+
except Exception:
|
|
232
|
+
return pd.DataFrame()
|
|
233
|
+
|
|
234
|
+
meta_df = _read(SimulationResultsSaver.METADATA_FILE)
|
|
235
|
+
if meta_df.empty:
|
|
236
|
+
raise FileNotFoundError(f"Metadata not found at '{run_path}'")
|
|
237
|
+
|
|
238
|
+
return SimulationResultsSaver._from_dfs(
|
|
239
|
+
meta=meta_df.iloc[0].to_dict(),
|
|
240
|
+
portfolio=_read(SimulationResultsSaver.DATA_FILES["portfolio"]),
|
|
241
|
+
executions=_read(SimulationResultsSaver.DATA_FILES["executions"]),
|
|
242
|
+
signals=_read(SimulationResultsSaver.DATA_FILES["signals"]),
|
|
243
|
+
targets=_read(SimulationResultsSaver.DATA_FILES["targets"]),
|
|
244
|
+
transfers=_read(SimulationResultsSaver.DATA_FILES["transfers"]),
|
|
245
|
+
emitter=_read(SimulationResultsSaver.DATA_FILES["emitter"]),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def load(self, backtest_id: str) -> TradingSessionResult:
|
|
249
|
+
"""
|
|
250
|
+
Load a TradingSessionResult by backtest_id.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
backtest_id: Relative path within base_path,
|
|
254
|
+
e.g. "my_strategy/Nimble/20240301_120000"
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
TradingSessionResult with all data loaded from parquet
|
|
258
|
+
"""
|
|
259
|
+
return self._load_from_path(f"{self.base_path}{backtest_id.strip('/')}/")
|
|
260
|
+
|
|
261
|
+
def load_best_variation(
|
|
262
|
+
self,
|
|
263
|
+
variation_set_id: str,
|
|
264
|
+
by: str = "sharpe",
|
|
265
|
+
ascending: bool = False,
|
|
266
|
+
) -> TradingSessionResult:
|
|
267
|
+
"""
|
|
268
|
+
Load the best-performing variation from a variation set.
|
|
269
|
+
|
|
270
|
+
The variation set _metadata.parquet has one row per variation.
|
|
271
|
+
Finds the best row by the given metric, then loads its data.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
variation_set_id: Relative path to variation set root,
|
|
275
|
+
e.g. "my_strategy/Nimble/20240301_130000"
|
|
276
|
+
by: Metric column to rank by (default: "sharpe")
|
|
277
|
+
ascending: If True, load minimum instead of maximum (default: False)
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
TradingSessionResult of the best variation
|
|
281
|
+
"""
|
|
282
|
+
meta_path = f"{self.base_path}{variation_set_id.strip('/')}/{SimulationResultsSaver.METADATA_FILE}"
|
|
283
|
+
order = "ASC" if ascending else "DESC"
|
|
284
|
+
row = self._conn.execute(f"SELECT * FROM read_parquet('{meta_path}') ORDER BY {by} {order} LIMIT 1").df()
|
|
285
|
+
|
|
286
|
+
if row.empty:
|
|
287
|
+
raise ValueError(f"No variations found at '{variation_set_id}'")
|
|
288
|
+
|
|
289
|
+
var_id = row["variation_id"].iloc[0]
|
|
290
|
+
run_path = f"{self.base_path}{variation_set_id.strip('/')}/{var_id}/"
|
|
291
|
+
return self._load_from_path(run_path)
|
|
292
|
+
|
|
293
|
+
def get_portfolio(
|
|
294
|
+
self,
|
|
295
|
+
backtest_id: str,
|
|
296
|
+
symbol: str | None = None,
|
|
297
|
+
start: str | None = None,
|
|
298
|
+
stop: str | None = None,
|
|
299
|
+
) -> pd.DataFrame:
|
|
300
|
+
"""
|
|
301
|
+
Get portfolio log data for a backtest.
|
|
302
|
+
|
|
303
|
+
Portfolio is stored in wide format: one column per symbol metric
|
|
304
|
+
(e.g. "BINANCE.UM:BTCUSDT_PnL", "BINANCE.UM:BTCUSDT_Commission").
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
backtest_id: Relative path within base_path
|
|
308
|
+
symbol: If set, returns only columns containing this symbol name (case-insensitive)
|
|
309
|
+
start: Start timestamp filter (inclusive)
|
|
310
|
+
stop: Stop timestamp filter (inclusive)
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
pd.DataFrame with portfolio data
|
|
314
|
+
"""
|
|
315
|
+
path = f"{self.base_path}{backtest_id.strip('/')}/{SimulationResultsSaver.DATA_FILES['portfolio']}"
|
|
316
|
+
return self._query_wide(path, symbol=symbol, start=start, stop=stop)
|
|
317
|
+
|
|
318
|
+
def get_executions(
|
|
319
|
+
self,
|
|
320
|
+
backtest_id: str,
|
|
321
|
+
symbol: str | None = None,
|
|
322
|
+
start: str | None = None,
|
|
323
|
+
stop: str | None = None,
|
|
324
|
+
) -> pd.DataFrame:
|
|
325
|
+
"""
|
|
326
|
+
Get execution log data for a backtest.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
backtest_id: Relative path within base_path
|
|
330
|
+
symbol: Filter rows by instrument column (case-insensitive match)
|
|
331
|
+
start: Start timestamp filter (inclusive)
|
|
332
|
+
stop: Stop timestamp filter (inclusive)
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
pd.DataFrame with execution data
|
|
336
|
+
"""
|
|
337
|
+
path = f"{self.base_path}{backtest_id.strip('/')}/{SimulationResultsSaver.DATA_FILES['executions']}"
|
|
338
|
+
return self._query_long(path, symbol=symbol, start=start, stop=stop)
|
|
339
|
+
|
|
340
|
+
def get_signals(
|
|
341
|
+
self,
|
|
342
|
+
backtest_id: str,
|
|
343
|
+
symbol: str | None = None,
|
|
344
|
+
start: str | None = None,
|
|
345
|
+
stop: str | None = None,
|
|
346
|
+
) -> pd.DataFrame:
|
|
347
|
+
"""
|
|
348
|
+
Get signals log data for a backtest.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
backtest_id: Relative path within base_path
|
|
352
|
+
symbol: Filter rows by instrument column (case-insensitive match)
|
|
353
|
+
start: Start timestamp filter (inclusive)
|
|
354
|
+
stop: Stop timestamp filter (inclusive)
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
pd.DataFrame with signals data
|
|
358
|
+
"""
|
|
359
|
+
path = f"{self.base_path}{backtest_id.strip('/')}/{SimulationResultsSaver.DATA_FILES['signals']}"
|
|
360
|
+
return self._query_long(path, symbol=symbol, start=start, stop=stop)
|
|
361
|
+
|
|
362
|
+
def _query_wide(
|
|
363
|
+
self,
|
|
364
|
+
path: str,
|
|
365
|
+
symbol: str | None = None,
|
|
366
|
+
start: str | None = None,
|
|
367
|
+
stop: str | None = None,
|
|
368
|
+
) -> pd.DataFrame:
|
|
369
|
+
"""
|
|
370
|
+
Query a wide-format parquet (portfolio log) with optional column/time filtering.
|
|
371
|
+
Symbol filtering selects columns containing the symbol string using DuckDB COLUMNS().
|
|
372
|
+
"""
|
|
373
|
+
conditions = []
|
|
374
|
+
if start:
|
|
375
|
+
conditions.append(f"timestamp >= '{start}'")
|
|
376
|
+
if stop:
|
|
377
|
+
conditions.append(f"timestamp <= '{stop}'")
|
|
378
|
+
where_clause = f" WHERE {' AND '.join(conditions)}" if conditions else ""
|
|
379
|
+
|
|
380
|
+
if symbol:
|
|
381
|
+
sym = symbol.upper()
|
|
382
|
+
# - DuckDB COLUMNS() lambda: select timestamp + _backtest_id + symbol columns
|
|
383
|
+
sql = f"""
|
|
384
|
+
SELECT COLUMNS(c -> c = 'timestamp' OR c = '_backtest_id'
|
|
385
|
+
OR contains(upper(c), '{sym}'))
|
|
386
|
+
FROM read_parquet('{path}'){where_clause}
|
|
387
|
+
"""
|
|
388
|
+
else:
|
|
389
|
+
sql = f"SELECT * FROM read_parquet('{path}'){where_clause}"
|
|
390
|
+
|
|
391
|
+
return self._conn.execute(sql).df()
|
|
392
|
+
|
|
393
|
+
def _query_long(
|
|
394
|
+
self,
|
|
395
|
+
path: str,
|
|
396
|
+
symbol: str | None = None,
|
|
397
|
+
start: str | None = None,
|
|
398
|
+
stop: str | None = None,
|
|
399
|
+
symbol_col: str = "symbol",
|
|
400
|
+
) -> pd.DataFrame:
|
|
401
|
+
"""
|
|
402
|
+
Query a long-format parquet (executions, signals) with optional row filtering.
|
|
403
|
+
Symbol filtering matches rows where symbol_col contains the symbol string.
|
|
404
|
+
"""
|
|
405
|
+
conditions = []
|
|
406
|
+
if start:
|
|
407
|
+
conditions.append(f"timestamp >= '{start}'")
|
|
408
|
+
if stop:
|
|
409
|
+
conditions.append(f"timestamp <= '{stop}'")
|
|
410
|
+
if symbol:
|
|
411
|
+
conditions.append(f"contains(upper({symbol_col}), '{symbol.upper()}')")
|
|
412
|
+
where_clause = f" WHERE {' AND '.join(conditions)}" if conditions else ""
|
|
413
|
+
sql = f"SELECT * FROM read_parquet('{path}'){where_clause}"
|
|
414
|
+
return self._conn.execute(sql).df()
|
|
415
|
+
|
|
416
|
+
def print(
|
|
417
|
+
self,
|
|
418
|
+
where: str | None = None,
|
|
419
|
+
order_by: str = "creation_time DESC",
|
|
420
|
+
limit: int | None = None,
|
|
421
|
+
params: bool = False,
|
|
422
|
+
) -> None:
|
|
423
|
+
"""
|
|
424
|
+
Pretty-print a colored list of backtests stored at base_path.
|
|
425
|
+
|
|
426
|
+
Matches the style of the old BacktestsResultsManager.list() — header line,
|
|
427
|
+
description, strategy / interval / capital / instruments, full metrics table.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
where: DuckDB WHERE clause to filter (e.g. ``"sharpe > 2"``). None = all.
|
|
431
|
+
order_by: ORDER BY clause (default: ``"creation_time DESC"``).
|
|
432
|
+
limit: Maximum number of results to display.
|
|
433
|
+
params: If True, print strategy parameters below the metrics table.
|
|
434
|
+
"""
|
|
435
|
+
df = self.search(where=where, order_by=order_by, limit=limit)
|
|
436
|
+
|
|
437
|
+
if df.empty:
|
|
438
|
+
print("No backtests found.")
|
|
439
|
+
return
|
|
440
|
+
|
|
441
|
+
_l = lambda v: [] if v is None else list(v) # noqa: E731 — numpy array → Python list
|
|
442
|
+
_METRIC_COLS = ["gain", "cagr", "sharpe", "qr", "mdd_pct", "mdd_usd", "fees", "execs"]
|
|
443
|
+
|
|
444
|
+
for _, row in df.iterrows():
|
|
445
|
+
_id = row.get("backtest_id", "")
|
|
446
|
+
_name = row.get("name", "")
|
|
447
|
+
_cls = str(row.get("strategy_class", "")).split(".")[-1]
|
|
448
|
+
_created = pd.Timestamp(row.get("creation_time")).strftime("%Y-%m-%d %H:%M:%S")
|
|
449
|
+
_author = row.get("author", "")
|
|
450
|
+
_start = pd.Timestamp(row.get("start")).strftime("%Y-%m-%d")
|
|
451
|
+
_stop = pd.Timestamp(row.get("stop")).strftime("%Y-%m-%d")
|
|
452
|
+
_capital = row.get("capital", "")
|
|
453
|
+
_ccy = row.get("base_currency", "")
|
|
454
|
+
_comm = row.get("commissions", "")
|
|
455
|
+
_dscr = row.get("description", "") or ""
|
|
456
|
+
_tags = _l(row.get("tags"))
|
|
457
|
+
_symbols = ", ".join(_l(row.get("symbols")))
|
|
458
|
+
_is_var = row.get("is_variation", False)
|
|
459
|
+
|
|
460
|
+
# - header: id :: name ::: created by author
|
|
461
|
+
_s = f"{yellow(_id)} :: {red(_name)}"
|
|
462
|
+
if _is_var:
|
|
463
|
+
_var_id = row.get("variation_id", "")
|
|
464
|
+
_var_params = row.get("variation_params", "") or ""
|
|
465
|
+
_s += f" [{cyan(_var_id)}] {magenta(_var_params)}"
|
|
466
|
+
_s += f" ::: {magenta(_created)} by {cyan(_author)}"
|
|
467
|
+
|
|
468
|
+
# - description lines
|
|
469
|
+
if _dscr:
|
|
470
|
+
for _d in _dscr.split("\n"):
|
|
471
|
+
if _d.strip():
|
|
472
|
+
_s += f"\n\t{magenta('# ' + _d)}"
|
|
473
|
+
|
|
474
|
+
_s += f"\n\tstrategy: {green(_cls)}"
|
|
475
|
+
_s += f"\n\tinterval: {blue(_start)} - {blue(_stop)}"
|
|
476
|
+
_s += f"\n\tcapital: {blue(str(_capital))} {_ccy} ({_comm})"
|
|
477
|
+
_s += f"\n\tinstruments: {blue(_symbols)}"
|
|
478
|
+
if _tags:
|
|
479
|
+
_s += f"\n\ttags: {cyan(str(_tags))}"
|
|
480
|
+
|
|
481
|
+
print(_s)
|
|
482
|
+
|
|
483
|
+
# - performance metrics table (red header, cyan values — same as old manager)
|
|
484
|
+
_metrics = {
|
|
485
|
+
c: (int(row.get(c) or 0) if c == "execs" else round(float(row.get(c) or 0.0), 3))
|
|
486
|
+
for c in _METRIC_COLS
|
|
487
|
+
if c in row
|
|
488
|
+
}
|
|
489
|
+
_m_df = pd.DataFrame([_metrics])
|
|
490
|
+
_m_str = _m_df.to_string(index=False)
|
|
491
|
+
_h, _v = _m_str.split("\n")
|
|
492
|
+
print("\t " + red(_h))
|
|
493
|
+
print("\t " + cyan(_v))
|
|
494
|
+
|
|
495
|
+
# - optional parameters
|
|
496
|
+
if params:
|
|
497
|
+
import json as _json
|
|
498
|
+
|
|
499
|
+
_p = _json.loads(row.get("parameters") or "{}")
|
|
500
|
+
if _p:
|
|
501
|
+
for k, v in _p.items():
|
|
502
|
+
print(f"\t {yellow(k)}: {cyan(str(v))}")
|
|
503
|
+
|
|
504
|
+
print()
|
|
505
|
+
|
|
506
|
+
def list(
|
|
507
|
+
self,
|
|
508
|
+
where: str | None = None,
|
|
509
|
+
order_by: str = "creation_time DESC",
|
|
510
|
+
limit: int | None = None,
|
|
511
|
+
) -> list[str]:
|
|
512
|
+
"""
|
|
513
|
+
Return a list of backtest IDs matching the given filter.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
where: Optional SQL WHERE clause to filter results.
|
|
517
|
+
order_by: SQL ORDER BY clause (default: ``creation_time DESC``).
|
|
518
|
+
limit: Maximum number of IDs to return.
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
List of backtest_id strings, e.g.
|
|
522
|
+
``["my_strategy/Nimble/20240301_120000", ...]``
|
|
523
|
+
"""
|
|
524
|
+
df = self.search(where=where, order_by=order_by, limit=limit)
|
|
525
|
+
if df.empty or "backtest_id" not in df.columns:
|
|
526
|
+
return []
|
|
527
|
+
return df["backtest_id"].tolist()
|
|
528
|
+
|
|
529
|
+
def export_backtests_to_markdown(self, backtest_id: str, path: str, tags: tuple[str] | None = None):
|
|
530
|
+
if tsr := self.load(backtest_id):
|
|
531
|
+
tsr.to_markdown(path, list(tags) if tags else None)
|