Qubx 0.6.85__tar.gz → 0.6.88__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of Qubx might be problematic. Click here for more details.
- {qubx-0.6.85 → qubx-0.6.88}/PKG-INFO +1 -1
- {qubx-0.6.85 → qubx-0.6.88}/pyproject.toml +1 -1
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/backtester/management.py +3 -2
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/backtester/runner.py +1 -1
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/cli/commands.py +46 -1
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +1 -1
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/handlers/funding_rate.py +3 -3
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/reader.py +3 -2
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/interfaces.py +7 -6
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/metrics.py +74 -14
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/emitters/base.py +23 -14
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/emitters/composite.py +13 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/emitters/csv.py +2 -1
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/emitters/indicator.py +4 -2
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/emitters/inmemory.py +5 -4
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/emitters/prometheus.py +2 -2
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/emitters/questdb.py +16 -10
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/exporters/formatters/__init__.py +8 -1
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/exporters/formatters/base.py +0 -1
- qubx-0.6.88/src/qubx/exporters/formatters/target_position.py +78 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/exporters/redis_streams.py +24 -4
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/health/base.py +7 -10
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/runner/configs.py +120 -17
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/runner/runner.py +6 -6
- {qubx-0.6.85 → qubx-0.6.88}/LICENSE +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/README.md +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/build.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/backtester/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/backtester/account.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/backtester/broker.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/backtester/data.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/backtester/ome.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/backtester/optimization.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/backtester/simulator.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/backtester/utils.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/cli/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/cli/deploy.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/cli/misc.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/cli/release.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/cli/tui.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/account.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/basics.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/context.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/deque.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/errors.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/exceptions.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/helpers.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/initializer.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/loggers.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/lookups.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/mixins/market.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/mixins/processing.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/mixins/utils.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/series.pxd +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/series.pyi +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/series.pyx +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/stale_data_detector.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/data/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/data/composite.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/data/helpers.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/data/hft.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/data/readers.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/data/registry.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/data/tardis.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/emitters/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/exporters/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/exporters/composite.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/exporters/slack.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/features/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/features/core.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/features/orderbook.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/features/price.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/features/trades.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/features/utils.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/gathering/simplest.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/health/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/loggers/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/loggers/csv.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/loggers/factory.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/loggers/mongo.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/math/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/math/stats.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/notifications/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/notifications/composite.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/notifications/slack.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/notifications/throttler.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/pandaz/ta.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/pandaz/utils.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/resources/_build.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/restarts/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/restorers/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/restorers/balance.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/restorers/factory.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/restorers/position.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/restorers/signal.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/restorers/state.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/restorers/utils.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/templates/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/templates/base.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/templates/project/template.yml +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/trackers/advanced.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/trackers/sizers.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/charting/orderbook.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/collections.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/misc.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/orderbook.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/questdb.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/time.py +0 -0
- {qubx-0.6.85 → qubx-0.6.88}/src/qubx/utils/version.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "Qubx"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.88"
|
|
8
8
|
description = "Qubx - Quantitative Trading Framework"
|
|
9
9
|
authors = [ "Dmitry Marienko <dmitry.marienko@xlydian.com>", "Yuriy Arabskyy <yuriy.arabskyy@xlydian.com>",]
|
|
10
10
|
readme = "README.md"
|
|
@@ -327,10 +327,11 @@ class BacktestsResultsManager:
|
|
|
327
327
|
if not as_table:
|
|
328
328
|
print(_s)
|
|
329
329
|
|
|
330
|
+
dd_column = "max_dd_pct" if "max_dd_pct" in metrics else "mdd_pct"
|
|
330
331
|
if with_metrics:
|
|
331
332
|
_m_repr = (
|
|
332
333
|
pd.DataFrame.from_dict(metrics, orient="index")
|
|
333
|
-
.T[["gain", "cagr", "sharpe", "qr",
|
|
334
|
+
.T[["gain", "cagr", "sharpe", "qr", dd_column, "mdd_usd", "fees", "execs"]]
|
|
334
335
|
.astype(float)
|
|
335
336
|
)
|
|
336
337
|
_m_repr = _m_repr.round(3).to_string(index=False)
|
|
@@ -345,7 +346,7 @@ class BacktestsResultsManager:
|
|
|
345
346
|
metrics = {
|
|
346
347
|
m: round(v, 3)
|
|
347
348
|
for m, v in metrics.items()
|
|
348
|
-
if m in ["gain", "cagr", "sharpe", "qr",
|
|
349
|
+
if m in ["gain", "cagr", "sharpe", "qr", dd_column, "mdd_usd", "fees", "execs"]
|
|
349
350
|
}
|
|
350
351
|
_t_rep.append(
|
|
351
352
|
{"Index": info.get("idx", ""), "Strategy": name}
|
|
@@ -137,6 +137,51 @@ def ls(directory: str):
|
|
|
137
137
|
ls_strats(directory)
|
|
138
138
|
|
|
139
139
|
|
|
140
|
+
@main.command()
|
|
141
|
+
@click.argument("config-file", type=Path, required=True)
|
|
142
|
+
@click.option(
|
|
143
|
+
"--no-check-imports",
|
|
144
|
+
is_flag=True,
|
|
145
|
+
default=False,
|
|
146
|
+
help="Skip checking if strategy class can be imported",
|
|
147
|
+
show_default=True,
|
|
148
|
+
)
|
|
149
|
+
def validate(config_file: Path, no_check_imports: bool):
|
|
150
|
+
"""
|
|
151
|
+
Validates a strategy configuration file without running it.
|
|
152
|
+
|
|
153
|
+
Checks for:
|
|
154
|
+
- Valid YAML syntax
|
|
155
|
+
- Required configuration fields
|
|
156
|
+
- Strategy class exists and can be imported (unless --no-check-imports)
|
|
157
|
+
- Exchange configurations are valid
|
|
158
|
+
- Simulation parameters are valid (if present)
|
|
159
|
+
|
|
160
|
+
Returns exit code 0 if valid, 1 if invalid.
|
|
161
|
+
"""
|
|
162
|
+
from qubx.utils.runner.configs import validate_strategy_config
|
|
163
|
+
|
|
164
|
+
result = validate_strategy_config(config_file, check_imports=not no_check_imports)
|
|
165
|
+
|
|
166
|
+
if result.valid:
|
|
167
|
+
click.echo(click.style("✓ Configuration is valid", fg="green", bold=True))
|
|
168
|
+
if result.warnings:
|
|
169
|
+
click.echo(click.style("\nWarnings:", fg="yellow", bold=True))
|
|
170
|
+
for warning in result.warnings:
|
|
171
|
+
click.echo(click.style(f" - {warning}", fg="yellow"))
|
|
172
|
+
raise SystemExit(0)
|
|
173
|
+
else:
|
|
174
|
+
click.echo(click.style("✗ Configuration is invalid", fg="red", bold=True))
|
|
175
|
+
click.echo(click.style("\nErrors:", fg="red", bold=True))
|
|
176
|
+
for error in result.errors:
|
|
177
|
+
click.echo(click.style(f" - {error}", fg="red"))
|
|
178
|
+
if result.warnings:
|
|
179
|
+
click.echo(click.style("\nWarnings:", fg="yellow", bold=True))
|
|
180
|
+
for warning in result.warnings:
|
|
181
|
+
click.echo(click.style(f" - {warning}", fg="yellow"))
|
|
182
|
+
raise SystemExit(1)
|
|
183
|
+
|
|
184
|
+
|
|
140
185
|
@main.command()
|
|
141
186
|
@click.argument(
|
|
142
187
|
"directory",
|
|
@@ -358,7 +403,7 @@ def init(
|
|
|
358
403
|
The generated strategy can be run immediately with:
|
|
359
404
|
poetry run qubx run --config config.yml --paper
|
|
360
405
|
"""
|
|
361
|
-
from qubx.templates import
|
|
406
|
+
from qubx.templates import TemplateError, TemplateManager
|
|
362
407
|
|
|
363
408
|
try:
|
|
364
409
|
manager = TemplateManager()
|
|
@@ -8,7 +8,7 @@ from ...adapters.polling_adapter import PollingConfig, PollingToWebSocketAdapter
|
|
|
8
8
|
from ..base import CcxtFuturePatchMixin
|
|
9
9
|
|
|
10
10
|
# Constants
|
|
11
|
-
FUNDING_RATE_DEFAULT_POLL_MINUTES =
|
|
11
|
+
FUNDING_RATE_DEFAULT_POLL_MINUTES = 1
|
|
12
12
|
FUNDING_RATE_HOUR_MS = 60 * 60 * 1000 # 1 hour in milliseconds
|
|
13
13
|
|
|
14
14
|
|
|
@@ -71,7 +71,7 @@ class FundingRateDataHandler(BaseDataTypeHandler):
|
|
|
71
71
|
channel.send((instrument, DataType.FUNDING_RATE, funding_rate, False))
|
|
72
72
|
|
|
73
73
|
# Emit payment if funding interval changed
|
|
74
|
-
if self._should_emit_payment(instrument, funding_rate):
|
|
74
|
+
if self._should_emit_payment(instrument, funding_rate, current_time):
|
|
75
75
|
payment = self._create_funding_payment(instrument)
|
|
76
76
|
channel.send((instrument, DataType.FUNDING_PAYMENT, payment, False))
|
|
77
77
|
|
|
@@ -101,7 +101,7 @@ class FundingRateDataHandler(BaseDataTypeHandler):
|
|
|
101
101
|
stream_name=name,
|
|
102
102
|
)
|
|
103
103
|
|
|
104
|
-
def _should_emit_payment(self, instrument: Instrument, rate: FundingRate) -> bool:
|
|
104
|
+
def _should_emit_payment(self, instrument: Instrument, rate: FundingRate, current_time: dt_64) -> bool:
|
|
105
105
|
"""
|
|
106
106
|
Determine if a funding payment should be emitted.
|
|
107
107
|
|
|
@@ -132,7 +132,7 @@ class FundingRateDataHandler(BaseDataTypeHandler):
|
|
|
132
132
|
return False
|
|
133
133
|
|
|
134
134
|
# Emit if next_funding_time has advanced (new funding period started)
|
|
135
|
-
if rate.next_funding_time > last_info["payment_time"]:
|
|
135
|
+
if rate.next_funding_time > last_info["payment_time"] and current_time > last_info["payment_time"]:
|
|
136
136
|
# Store payment info for _create_funding_payment
|
|
137
137
|
self._pending_funding_rates[f"{key}_payment"] = {
|
|
138
138
|
"rate": last_info["rate"].rate,
|
|
@@ -20,7 +20,7 @@ from .utils import ccxt_find_instrument, instrument_to_ccxt_symbol
|
|
|
20
20
|
|
|
21
21
|
@reader("ccxt")
|
|
22
22
|
class CcxtDataReader(DataReader):
|
|
23
|
-
SUPPORTED_DATA_TYPES = {"ohlc"
|
|
23
|
+
SUPPORTED_DATA_TYPES = {"ohlc"}
|
|
24
24
|
|
|
25
25
|
_exchanges: dict[str, Exchange]
|
|
26
26
|
_loop: AsyncThreadLoop
|
|
@@ -74,7 +74,8 @@ class CcxtDataReader(DataReader):
|
|
|
74
74
|
if instrument is None:
|
|
75
75
|
return []
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
timeframe = timeframe or "1m"
|
|
78
|
+
_timeframe = pd.Timedelta(timeframe)
|
|
78
79
|
_start, _stop = self._get_start_stop(start, stop, _timeframe)
|
|
79
80
|
|
|
80
81
|
if _start > _stop:
|
|
@@ -2050,7 +2050,7 @@ class IMetricEmitter:
|
|
|
2050
2050
|
self,
|
|
2051
2051
|
name: str,
|
|
2052
2052
|
value: float,
|
|
2053
|
-
tags: dict[str,
|
|
2053
|
+
tags: dict[str, Any] | None = None,
|
|
2054
2054
|
timestamp: dt_64 | None = None,
|
|
2055
2055
|
instrument: Instrument | None = None,
|
|
2056
2056
|
) -> None:
|
|
@@ -2092,15 +2092,16 @@ class IMetricEmitter:
|
|
|
2092
2092
|
"""
|
|
2093
2093
|
pass
|
|
2094
2094
|
|
|
2095
|
-
def
|
|
2095
|
+
def set_context(self, context: "IStrategyContext") -> None:
|
|
2096
2096
|
"""
|
|
2097
|
-
Set the
|
|
2097
|
+
Set the strategy context for the metric emitter.
|
|
2098
2098
|
|
|
2099
|
-
This method is used to set the
|
|
2100
|
-
|
|
2099
|
+
This method is used to set the context that provides access to time and simulation state.
|
|
2100
|
+
The context is used to automatically add is_live tag and get timestamps when no explicit
|
|
2101
|
+
timestamp is provided in the emit method.
|
|
2101
2102
|
|
|
2102
2103
|
Args:
|
|
2103
|
-
|
|
2104
|
+
context: The strategy context to use
|
|
2104
2105
|
"""
|
|
2105
2106
|
pass
|
|
2106
2107
|
|
|
@@ -175,7 +175,7 @@ def cagr(returns, periods=DAILY):
|
|
|
175
175
|
|
|
176
176
|
cumrets = (returns + 1).cumprod(axis=0)
|
|
177
177
|
years = len(cumrets) / float(periods)
|
|
178
|
-
return (cumrets.iloc[-1] ** (1.0 / years)) - 1.0
|
|
178
|
+
return ((cumrets.iloc[-1] ** (1.0 / years)) - 1.0) * 100
|
|
179
179
|
|
|
180
180
|
|
|
181
181
|
def calmar_ratio(returns, periods=DAILY):
|
|
@@ -747,6 +747,11 @@ class TradingSessionResult:
|
|
|
747
747
|
"""Get number of executions"""
|
|
748
748
|
return len(self.executions_log)
|
|
749
749
|
|
|
750
|
+
@property
|
|
751
|
+
def turnover(self) -> float:
|
|
752
|
+
"""Get average daily turnover as percentage of equity"""
|
|
753
|
+
return self.performance().get("avg_daily_turnover", 0.0)
|
|
754
|
+
|
|
750
755
|
@property
|
|
751
756
|
def leverage(self) -> pd.Series:
|
|
752
757
|
"""Get leverage over time"""
|
|
@@ -779,7 +784,7 @@ class TradingSessionResult:
|
|
|
779
784
|
for k in [
|
|
780
785
|
"equity", "drawdown_usd", "drawdown_pct",
|
|
781
786
|
"compound_returns", "returns_daily", "returns", "monthly_returns",
|
|
782
|
-
"rolling_sharpe", "long_value", "short_value",
|
|
787
|
+
"rolling_sharpe", "long_value", "short_value", "turnover",
|
|
783
788
|
]:
|
|
784
789
|
self._metrics.pop(k, None)
|
|
785
790
|
# fmt: on
|
|
@@ -1381,16 +1386,21 @@ def portfolio_metrics(
|
|
|
1381
1386
|
execs = len(executions_log)
|
|
1382
1387
|
mdd_pct = 100 * dd_data / equity.cummax() if execs > 0 else pd.Series(0, index=equity.index)
|
|
1383
1388
|
sheet["equity"] = equity
|
|
1384
|
-
sheet["gain"] = sheet["equity"].iloc[-1] - sheet["equity"].iloc[0]
|
|
1385
|
-
sheet["cagr"] = cagr(returns_daily, performance_statistics_period)
|
|
1386
1389
|
sheet["sharpe"] = sharpe_ratio(returns_daily, risk_free, performance_statistics_period)
|
|
1390
|
+
sheet["cagr"] = cagr(returns_daily, performance_statistics_period)
|
|
1391
|
+
|
|
1392
|
+
# turnover calculation
|
|
1393
|
+
symbols = list(set(portfolio_log.columns.str.split("_").str.get(0).values))
|
|
1394
|
+
turnover_series = calculate_turnover(portfolio_log, symbols, equity, resample="1d")
|
|
1395
|
+
sheet["turnover"] = turnover_series
|
|
1396
|
+
sheet["daily_turnover"] = turnover_series.mean() if len(turnover_series) > 0 else 0.0
|
|
1397
|
+
|
|
1387
1398
|
sheet["qr"] = qr(equity) if execs > 0 else 0
|
|
1388
|
-
sheet["
|
|
1399
|
+
sheet["mdd_pct"] = max(mdd_pct)
|
|
1389
1400
|
sheet["drawdown_pct"] = mdd_pct
|
|
1401
|
+
sheet["drawdown_usd"] = dd_data
|
|
1390
1402
|
# 25-May-2019: MDE fixed Max DD pct calculations
|
|
1391
|
-
sheet["max_dd_pct"] = max(mdd_pct)
|
|
1392
1403
|
# sheet["max_dd_pct_on_init"] = 100 * mdd / init_cash
|
|
1393
|
-
sheet["mdd_usd"] = mdd
|
|
1394
1404
|
sheet["mdd_start"] = equity.index[ddstart]
|
|
1395
1405
|
sheet["mdd_peak"] = equity.index[ddpeak]
|
|
1396
1406
|
sheet["mdd_recover"] = equity.index[ddrecover]
|
|
@@ -1403,12 +1413,12 @@ def portfolio_metrics(
|
|
|
1403
1413
|
)
|
|
1404
1414
|
sheet["calmar"] = calmar_ratio(returns_daily, performance_statistics_period)
|
|
1405
1415
|
# sheet["ann_vol"] = annual_volatility(returns_daily)
|
|
1406
|
-
sheet["tail_ratio"] = tail_ratio(returns_daily)
|
|
1407
|
-
sheet["stability"] = stability_of_returns(returns_daily)
|
|
1416
|
+
# sheet["tail_ratio"] = tail_ratio(returns_daily)
|
|
1417
|
+
# sheet["stability"] = stability_of_returns(returns_daily)
|
|
1408
1418
|
sheet["monthly_returns"] = aggregate_returns(returns_daily, convert_to="mon")
|
|
1409
1419
|
r_m = np.mean(returns_daily)
|
|
1410
1420
|
r_s = np.std(returns_daily)
|
|
1411
|
-
sheet["var"] = var_cov_var(init_cash, r_m, r_s)
|
|
1421
|
+
# sheet["var"] = var_cov_var(init_cash, r_m, r_s)
|
|
1412
1422
|
sheet["avg_return"] = 100 * r_m
|
|
1413
1423
|
|
|
1414
1424
|
# portfolio market values
|
|
@@ -1416,6 +1426,8 @@ def portfolio_metrics(
|
|
|
1416
1426
|
sheet["long_value"] = mkt_value[mkt_value > 0].sum(axis=1).fillna(0)
|
|
1417
1427
|
sheet["short_value"] = mkt_value[mkt_value < 0].sum(axis=1).fillna(0)
|
|
1418
1428
|
|
|
1429
|
+
sheet["gain"] = sheet["equity"].iloc[-1] - sheet["equity"].iloc[0]
|
|
1430
|
+
sheet["mdd_usd"] = mdd
|
|
1419
1431
|
# total commissions
|
|
1420
1432
|
sheet["fees"] = pft_total["Total_Commissions"].iloc[-1]
|
|
1421
1433
|
|
|
@@ -1423,9 +1435,10 @@ def portfolio_metrics(
|
|
|
1423
1435
|
funding_columns = pft_total.filter(regex=".*_Funding")
|
|
1424
1436
|
if not funding_columns.empty:
|
|
1425
1437
|
total_funding = funding_columns.sum(axis=1)
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1438
|
+
if total_funding.iloc[-1] != 0:
|
|
1439
|
+
sheet["funding_pnl"] = 100 * total_funding.iloc[-1] / init_cash # as percentage of initial capital
|
|
1440
|
+
# else:
|
|
1441
|
+
# sheet["funding_pnl"] = 0.0
|
|
1429
1442
|
|
|
1430
1443
|
# executions metrics
|
|
1431
1444
|
sheet["execs"] = execs
|
|
@@ -1725,7 +1738,7 @@ def _tearsheet_single(
|
|
|
1725
1738
|
ay = sbp(_n, 5)
|
|
1726
1739
|
plt.plot(lev, c="c", lw=1.5, label="Leverage")
|
|
1727
1740
|
plt.subplots_adjust(hspace=0)
|
|
1728
|
-
return pd.DataFrame(report).T.round(
|
|
1741
|
+
return pd.DataFrame(report).T.round(2)
|
|
1729
1742
|
|
|
1730
1743
|
|
|
1731
1744
|
def calculate_leverage(
|
|
@@ -1828,6 +1841,53 @@ def calculate_pnl_per_symbol(
|
|
|
1828
1841
|
return df
|
|
1829
1842
|
|
|
1830
1843
|
|
|
1844
|
+
def calculate_turnover(
|
|
1845
|
+
portfolio_log: pd.DataFrame,
|
|
1846
|
+
symbols: list[str],
|
|
1847
|
+
equity: pd.Series,
|
|
1848
|
+
resample: str = "1d",
|
|
1849
|
+
) -> pd.Series:
|
|
1850
|
+
"""
|
|
1851
|
+
Calculate daily turnover as percentage of equity.
|
|
1852
|
+
|
|
1853
|
+
Turnover measures trading activity by calculating the absolute value of position changes
|
|
1854
|
+
multiplied by price, then dividing by equity.
|
|
1855
|
+
|
|
1856
|
+
Args:
|
|
1857
|
+
portfolio_log: Portfolio log dataframe with position and price columns
|
|
1858
|
+
symbols: List of symbols to calculate turnover for
|
|
1859
|
+
equity: Equity curve series
|
|
1860
|
+
resample: Resampling period for turnover calculation (default "1d")
|
|
1861
|
+
|
|
1862
|
+
Returns:
|
|
1863
|
+
pd.Series: Daily turnover as percentage of equity
|
|
1864
|
+
"""
|
|
1865
|
+
position_diffs = []
|
|
1866
|
+
|
|
1867
|
+
for symbol in symbols:
|
|
1868
|
+
pos_col = f"{symbol}_Pos"
|
|
1869
|
+
price_col = f"{symbol}_Price"
|
|
1870
|
+
|
|
1871
|
+
if pos_col in portfolio_log.columns and price_col in portfolio_log.columns:
|
|
1872
|
+
# Calculate absolute position change multiplied by price (notional value)
|
|
1873
|
+
position_diff = portfolio_log[pos_col].diff().abs() * portfolio_log[price_col]
|
|
1874
|
+
position_diffs.append(position_diff)
|
|
1875
|
+
|
|
1876
|
+
if not position_diffs:
|
|
1877
|
+
return pd.Series(0, index=equity.index)
|
|
1878
|
+
|
|
1879
|
+
# Sum all position changes and resample to specified period
|
|
1880
|
+
notional_turnover = pd.concat(position_diffs, axis=1).sum(axis=1).resample(resample).sum()
|
|
1881
|
+
|
|
1882
|
+
# Resample equity to match turnover frequency
|
|
1883
|
+
equity_resampled = equity.resample(resample).last()
|
|
1884
|
+
|
|
1885
|
+
# Calculate turnover as percentage of equity
|
|
1886
|
+
daily_turnover = notional_turnover.div(equity_resampled).mul(100).fillna(0)
|
|
1887
|
+
|
|
1888
|
+
return daily_turnover
|
|
1889
|
+
|
|
1890
|
+
|
|
1831
1891
|
def chart_signals(
|
|
1832
1892
|
result: TradingSessionResult,
|
|
1833
1893
|
symbol: str,
|
|
@@ -4,13 +4,13 @@ Base Metric Emitter.
|
|
|
4
4
|
This module provides a base implementation of IMetricEmitter that can be extended by other emitters.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import Dict, List, Optional, Set
|
|
7
|
+
from typing import Any, Dict, List, Optional, Set
|
|
8
8
|
|
|
9
9
|
import pandas as pd
|
|
10
10
|
|
|
11
11
|
from qubx import logger
|
|
12
12
|
from qubx.core.basics import Instrument, Signal, TargetPosition, dt_64
|
|
13
|
-
from qubx.core.interfaces import IAccountViewer, IMetricEmitter, IStrategyContext
|
|
13
|
+
from qubx.core.interfaces import IAccountViewer, IMetricEmitter, IStrategyContext
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class BaseMetricEmitter(IMetricEmitter):
|
|
@@ -35,7 +35,7 @@ class BaseMetricEmitter(IMetricEmitter):
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
def __init__(
|
|
38
|
-
self, stats_to_emit: Optional[List[str]] = None, stats_interval: str = "1m", tags: dict[str,
|
|
38
|
+
self, stats_to_emit: Optional[List[str]] = None, stats_interval: str = "1m", tags: dict[str, Any] | None = None
|
|
39
39
|
):
|
|
40
40
|
"""
|
|
41
41
|
Initialize the Base Metric Emitter.
|
|
@@ -49,18 +49,19 @@ class BaseMetricEmitter(IMetricEmitter):
|
|
|
49
49
|
self._stats_interval = pd.Timedelta(stats_interval)
|
|
50
50
|
self._default_tags = tags or {}
|
|
51
51
|
self._last_emission_time = None
|
|
52
|
-
self.
|
|
52
|
+
self._context = None
|
|
53
53
|
|
|
54
|
-
def _merge_tags(self, tags: dict[str,
|
|
54
|
+
def _merge_tags(self, tags: dict[str, Any] | None = None, instrument: Instrument | None = None) -> dict[str, Any]:
|
|
55
55
|
"""
|
|
56
56
|
Merge default tags with provided tags and instrument tags if provided.
|
|
57
|
+
Also automatically adds is_live tag based on context's simulation state.
|
|
57
58
|
|
|
58
59
|
Args:
|
|
59
60
|
tags: Additional tags to merge with default tags
|
|
60
61
|
instrument: Optional instrument to add symbol and exchange tags from
|
|
61
62
|
|
|
62
63
|
Returns:
|
|
63
|
-
Dict[str,
|
|
64
|
+
Dict[str, Any]: Merged tags dictionary
|
|
64
65
|
"""
|
|
65
66
|
result = self._default_tags.copy()
|
|
66
67
|
|
|
@@ -70,9 +71,13 @@ class BaseMetricEmitter(IMetricEmitter):
|
|
|
70
71
|
if instrument:
|
|
71
72
|
result.update({"symbol": instrument.symbol, "exchange": instrument.exchange})
|
|
72
73
|
|
|
74
|
+
# Add is_live tag based on context's simulation state
|
|
75
|
+
if self._context is not None:
|
|
76
|
+
result["is_live"] = not self._context.is_simulation
|
|
77
|
+
|
|
73
78
|
return result
|
|
74
79
|
|
|
75
|
-
def _emit_impl(self, name: str, value: float, tags: Dict[str,
|
|
80
|
+
def _emit_impl(self, name: str, value: float, tags: Dict[str, Any], timestamp: dt_64 | None = None) -> None:
|
|
76
81
|
"""
|
|
77
82
|
Implementation of emit to be overridden by subclasses.
|
|
78
83
|
|
|
@@ -88,7 +93,7 @@ class BaseMetricEmitter(IMetricEmitter):
|
|
|
88
93
|
self,
|
|
89
94
|
name: str,
|
|
90
95
|
value: float,
|
|
91
|
-
tags: dict[str,
|
|
96
|
+
tags: dict[str, Any] | None = None,
|
|
92
97
|
timestamp: dt_64 | None = None,
|
|
93
98
|
instrument: Instrument | None = None,
|
|
94
99
|
) -> None:
|
|
@@ -102,19 +107,19 @@ class BaseMetricEmitter(IMetricEmitter):
|
|
|
102
107
|
timestamp: Optional timestamp for the metric (defaults to current time)
|
|
103
108
|
instrument: Optional instrument to add symbol and exchange tags from
|
|
104
109
|
"""
|
|
105
|
-
if self.
|
|
106
|
-
timestamp = self.
|
|
110
|
+
if self._context is not None and timestamp is None:
|
|
111
|
+
timestamp = self._context.time()
|
|
107
112
|
merged_tags = self._merge_tags(tags, instrument)
|
|
108
113
|
self._emit_impl(name, float(value), merged_tags, timestamp)
|
|
109
114
|
|
|
110
|
-
def
|
|
115
|
+
def set_context(self, context: IStrategyContext) -> None:
|
|
111
116
|
"""
|
|
112
|
-
Set the
|
|
117
|
+
Set the strategy context for the metric emitter.
|
|
113
118
|
|
|
114
119
|
Args:
|
|
115
|
-
|
|
120
|
+
context: The strategy context to use
|
|
116
121
|
"""
|
|
117
|
-
self.
|
|
122
|
+
self._context = context
|
|
118
123
|
|
|
119
124
|
def emit_strategy_stats(self, context: IStrategyContext) -> None:
|
|
120
125
|
"""
|
|
@@ -126,6 +131,10 @@ class BaseMetricEmitter(IMetricEmitter):
|
|
|
126
131
|
Args:
|
|
127
132
|
context: The strategy context to get statistics from
|
|
128
133
|
"""
|
|
134
|
+
# Store context to ensure is_live tag is added
|
|
135
|
+
if self._context is None:
|
|
136
|
+
self._context = context
|
|
137
|
+
|
|
129
138
|
try:
|
|
130
139
|
# Get current timestamp
|
|
131
140
|
current_time = context.time()
|
|
@@ -85,6 +85,19 @@ class CompositeMetricEmitter(BaseMetricEmitter):
|
|
|
85
85
|
except Exception as e:
|
|
86
86
|
logger.error(f"Error emitting signals to {emitter.__class__.__name__}: {e}")
|
|
87
87
|
|
|
88
|
+
def set_context(self, context: IStrategyContext) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Set the strategy context for all child emitters.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
context: The strategy context to use
|
|
94
|
+
"""
|
|
95
|
+
for emitter in self._emitters:
|
|
96
|
+
try:
|
|
97
|
+
emitter.set_context(context)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error(f"Error setting context on {emitter.__class__.__name__}: {e}")
|
|
100
|
+
|
|
88
101
|
def notify(self, context: IStrategyContext) -> None:
|
|
89
102
|
for emitter in self._emitters:
|
|
90
103
|
try:
|
|
@@ -6,6 +6,7 @@ This module provides an implementation of IMetricEmitter that exports metrics to
|
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
8
|
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
9
10
|
|
|
10
11
|
from qubx import logger
|
|
11
12
|
from qubx.core.basics import Signal, dt_64
|
|
@@ -27,7 +28,7 @@ class CSVMetricEmitter(BaseMetricEmitter):
|
|
|
27
28
|
file_path: str | None = None,
|
|
28
29
|
stats_to_emit: list[str] | None = None,
|
|
29
30
|
stats_interval: str = "1m",
|
|
30
|
-
tags: dict[str,
|
|
31
|
+
tags: dict[str, Any] | None = None,
|
|
31
32
|
):
|
|
32
33
|
"""
|
|
33
34
|
Initialize the CSV Metric Emitter.
|
|
@@ -5,6 +5,8 @@ This module provides the IndicatorEmitter class that can wrap around any indicat
|
|
|
5
5
|
and automatically emit their values when there are updates.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
8
10
|
import numpy as np
|
|
9
11
|
import pandas as pd
|
|
10
12
|
|
|
@@ -43,7 +45,7 @@ class IndicatorEmitter(Indicator):
|
|
|
43
45
|
metric_emitter: IMetricEmitter,
|
|
44
46
|
metric_name: str | None = None,
|
|
45
47
|
instrument: Instrument | None = None,
|
|
46
|
-
tags: dict[str,
|
|
48
|
+
tags: dict[str, Any] | None = None,
|
|
47
49
|
emit_on_new_item_only: bool = True,
|
|
48
50
|
):
|
|
49
51
|
"""
|
|
@@ -149,7 +151,7 @@ class IndicatorEmitter(Indicator):
|
|
|
149
151
|
metric_emitter: IMetricEmitter,
|
|
150
152
|
metric_name: str | None = None,
|
|
151
153
|
instrument: Instrument | None = None,
|
|
152
|
-
tags: dict[str,
|
|
154
|
+
tags: dict[str, Any] | None = None,
|
|
153
155
|
emit_on_new_item_only: bool = True,
|
|
154
156
|
) -> "Indicator":
|
|
155
157
|
"""
|
|
@@ -5,7 +5,7 @@ This module provides an implementation of IMetricEmitter that stores metrics in
|
|
|
5
5
|
using a pandas DataFrame for easy access and analysis.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Any, cast
|
|
9
9
|
|
|
10
10
|
import pandas as pd
|
|
11
11
|
|
|
@@ -28,7 +28,7 @@ class InMemoryMetricEmitter(BaseMetricEmitter):
|
|
|
28
28
|
self,
|
|
29
29
|
stats_to_emit: list[str] | None = None,
|
|
30
30
|
stats_interval: str = "1m",
|
|
31
|
-
tags: dict[str,
|
|
31
|
+
tags: dict[str, Any] | None = None,
|
|
32
32
|
max_rows: int | None = None,
|
|
33
33
|
):
|
|
34
34
|
"""
|
|
@@ -135,7 +135,7 @@ class InMemoryMetricEmitter(BaseMetricEmitter):
|
|
|
135
135
|
if not self._rows:
|
|
136
136
|
df = pd.DataFrame(columns=["timestamp", "name", "value", "symbol", "exchange"])
|
|
137
137
|
else:
|
|
138
|
-
df = pd.DataFrame(self._rows)
|
|
138
|
+
df = pd.DataFrame(self._rows.copy())
|
|
139
139
|
# Ensure correct dtypes
|
|
140
140
|
df = df.astype(
|
|
141
141
|
{
|
|
@@ -163,7 +163,8 @@ class InMemoryMetricEmitter(BaseMetricEmitter):
|
|
|
163
163
|
df = df[df["timestamp"] >= start_time]
|
|
164
164
|
if end_time is not None:
|
|
165
165
|
df = df[df["timestamp"] <= end_time]
|
|
166
|
-
|
|
166
|
+
|
|
167
|
+
return cast(pd.DataFrame, df)
|
|
167
168
|
|
|
168
169
|
def get_latest_metrics(
|
|
169
170
|
self, instrument: Instrument | None = None, symbol: str | None = None, exchange: str | None = None
|
|
@@ -4,7 +4,7 @@ Prometheus Metric Emitter.
|
|
|
4
4
|
This module provides an implementation of IMetricEmitter that exports metrics to Prometheus.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import Dict, List, Literal, Optional
|
|
7
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
8
8
|
|
|
9
9
|
from prometheus_client import REGISTRY, Counter, Gauge, Summary, push_to_gateway
|
|
10
10
|
|
|
@@ -178,7 +178,7 @@ class PrometheusMetricEmitter(BaseMetricEmitter):
|
|
|
178
178
|
self,
|
|
179
179
|
name: str,
|
|
180
180
|
value: float,
|
|
181
|
-
tags: dict[str,
|
|
181
|
+
tags: dict[str, Any] | None = None,
|
|
182
182
|
timestamp: dt_64 | None = None,
|
|
183
183
|
metric_type: MetricType = "gauge",
|
|
184
184
|
) -> None:
|
|
@@ -6,6 +6,7 @@ This module provides an implementation of IMetricEmitter that exports metrics to
|
|
|
6
6
|
|
|
7
7
|
import datetime
|
|
8
8
|
from concurrent.futures import ThreadPoolExecutor
|
|
9
|
+
from typing import Any
|
|
9
10
|
|
|
10
11
|
import pandas as pd
|
|
11
12
|
from questdb.ingress import Sender
|
|
@@ -24,6 +25,8 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
|
|
|
24
25
|
This emitter sends metrics to QuestDB with custom timestamps and tags.
|
|
25
26
|
"""
|
|
26
27
|
|
|
28
|
+
SYMBOL_TAGS = ["symbol", "exchange", "type", "environment", "strategy"]
|
|
29
|
+
|
|
27
30
|
def __init__(
|
|
28
31
|
self,
|
|
29
32
|
host: str = "localhost",
|
|
@@ -33,7 +36,7 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
|
|
|
33
36
|
stats_to_emit: list[str] | None = None,
|
|
34
37
|
stats_interval: str = "1m",
|
|
35
38
|
flush_interval: str = "5s",
|
|
36
|
-
tags: dict[str,
|
|
39
|
+
tags: dict[str, Any] | None = None,
|
|
37
40
|
max_workers: int = 1,
|
|
38
41
|
):
|
|
39
42
|
"""
|
|
@@ -143,10 +146,8 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
|
|
|
143
146
|
return
|
|
144
147
|
|
|
145
148
|
# Prepare symbols (tags) and columns (values)
|
|
146
|
-
symbols =
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
columns: dict = {"value": round(value, 5)} # Add the value as a column
|
|
149
|
+
symbols = self._pop_symbols(tags)
|
|
150
|
+
columns: dict = {"metric_name": name, "value": round(value, 5), **tags}
|
|
150
151
|
|
|
151
152
|
# Use the provided timestamp if available, otherwise use current time
|
|
152
153
|
dt_timestamp = self._convert_timestamp(timestamp) if timestamp is not None else datetime.datetime.now()
|
|
@@ -261,11 +262,7 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
|
|
|
261
262
|
|
|
262
263
|
# Use _merge_tags to get properly merged tags
|
|
263
264
|
merged_tags = self._merge_tags({}, signal.instrument)
|
|
264
|
-
|
|
265
|
-
symbols = {
|
|
266
|
-
"group_name": signal.group if signal.group else "",
|
|
267
|
-
}
|
|
268
|
-
symbols.update(merged_tags) # Add merged tags
|
|
265
|
+
symbols = self._pop_symbols(merged_tags)
|
|
269
266
|
|
|
270
267
|
columns = {
|
|
271
268
|
"signal": float(signal.signal),
|
|
@@ -277,6 +274,8 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
|
|
|
277
274
|
"comment": signal.comment if signal.comment else "",
|
|
278
275
|
# "options": json.dumps(signal.options) if signal.options else "{}",
|
|
279
276
|
"is_service": bool(signal.is_service),
|
|
277
|
+
"group_name": signal.group if signal.group else "",
|
|
278
|
+
**merged_tags,
|
|
280
279
|
}
|
|
281
280
|
|
|
282
281
|
# Convert timestamp - signal.time is always dt_64, no need to check for string
|
|
@@ -287,3 +286,10 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
|
|
|
287
286
|
|
|
288
287
|
except Exception as e:
|
|
289
288
|
logger.error(f"[QuestDBMetricEmitter] Failed to emit signals to QuestDB: {e}")
|
|
289
|
+
|
|
290
|
+
def _pop_symbols(self, tags: dict[str, str]) -> dict[str, str]:
|
|
291
|
+
symbols = {}
|
|
292
|
+
for symbol_name in self.SYMBOL_TAGS:
|
|
293
|
+
if symbol_name in tags:
|
|
294
|
+
symbols[symbol_name] = tags.pop(symbol_name)
|
|
295
|
+
return symbols
|