Qubx 0.6.74__tar.gz → 0.6.76__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.74 → qubx-0.6.76}/PKG-INFO +1 -1
- {qubx-0.6.74 → qubx-0.6.76}/pyproject.toml +1 -1
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/handlers/ohlc.py +32 -9
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/reader.py +9 -4
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/mixins/market.py +13 -1
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/mixins/processing.py +4 -2
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/exporters/formatters/incremental.py +8 -8
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/ta/indicators.pxd +9 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/ta/indicators.pyi +10 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/ta/indicators.pyx +153 -1
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/trackers/riskctrl.py +1 -1
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/charting/mpl_helpers.py +45 -51
- qubx-0.6.76/src/qubx/utils/questdb.py +142 -0
- qubx-0.6.74/src/qubx/utils/questdb.py +0 -79
- {qubx-0.6.74 → qubx-0.6.76}/LICENSE +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/README.md +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/build.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/backtester/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/backtester/account.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/backtester/broker.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/backtester/data.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/backtester/management.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/backtester/ome.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/backtester/optimization.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/backtester/runner.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/backtester/simulator.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/backtester/utils.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/cli/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/cli/commands.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/cli/deploy.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/cli/misc.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/cli/release.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/cli/tui.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/account.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/basics.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/context.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/deque.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/errors.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/exceptions.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/helpers.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/initializer.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/interfaces.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/loggers.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/lookups.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/metrics.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/mixins/utils.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/series.pxd +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/series.pyi +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/series.pyx +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/stale_data_detector.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/data/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/data/composite.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/data/helpers.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/data/hft.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/data/readers.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/data/registry.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/data/tardis.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/emitters/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/emitters/base.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/emitters/composite.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/emitters/csv.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/emitters/indicator.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/emitters/questdb.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/exporters/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/exporters/composite.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/exporters/slack.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/features/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/features/core.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/features/orderbook.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/features/price.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/features/trades.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/features/utils.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/gathering/simplest.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/health/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/health/base.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/loggers/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/loggers/csv.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/loggers/factory.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/loggers/mongo.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/math/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/math/stats.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/notifications/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/notifications/composite.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/notifications/slack.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/notifications/throttler.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/pandaz/ta.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/pandaz/utils.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/resources/_build.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/restarts/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/restorers/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/restorers/balance.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/restorers/factory.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/restorers/position.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/restorers/signal.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/restorers/state.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/restorers/utils.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/templates/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/templates/base.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/templates/project/template.yml +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/trackers/advanced.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/trackers/sizers.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/charting/orderbook.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/collections.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/misc.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/orderbook.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/runner/configs.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/runner/runner.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/src/qubx/utils/time.py +0 -0
- {qubx-0.6.74 → qubx-0.6.76}/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.76"
|
|
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"
|
|
@@ -89,7 +89,9 @@ class OhlcDataHandler(BaseDataTypeHandler):
|
|
|
89
89
|
for instrument in instruments:
|
|
90
90
|
start = self._data_provider._time_msec_nbars_back(timeframe, nbarsback)
|
|
91
91
|
ccxt_symbol = instrument_to_ccxt_symbol(instrument)
|
|
92
|
-
ohlcv = await self._exchange_manager.exchange.fetch_ohlcv(
|
|
92
|
+
ohlcv = await self._exchange_manager.exchange.fetch_ohlcv(
|
|
93
|
+
ccxt_symbol, exch_timeframe, since=start, limit=nbarsback + 1
|
|
94
|
+
)
|
|
93
95
|
|
|
94
96
|
logger.debug(f"<yellow>{self._exchange_id}</yellow> {instrument}: loaded {len(ohlcv)} {timeframe} bars")
|
|
95
97
|
|
|
@@ -102,6 +104,12 @@ class OhlcDataHandler(BaseDataTypeHandler):
|
|
|
102
104
|
)
|
|
103
105
|
)
|
|
104
106
|
|
|
107
|
+
if len(ohlcv) > 0:
|
|
108
|
+
# Send a quote update to the context at the end of warmup
|
|
109
|
+
channel.send((instrument, DataType.QUOTE, self._convert_ohlcv_to_quote(ohlcv, instrument), False))
|
|
110
|
+
|
|
111
|
+
self._update_quote(instrument, ohlcv)
|
|
112
|
+
|
|
105
113
|
async def get_historical_ohlc(self, instrument: Instrument, timeframe: str, nbarsback: int) -> list[Bar]:
|
|
106
114
|
"""
|
|
107
115
|
Get historical OHLC data for a single instrument (used by get_ohlc method).
|
|
@@ -121,7 +129,9 @@ class OhlcDataHandler(BaseDataTypeHandler):
|
|
|
121
129
|
|
|
122
130
|
# Retrieve OHLC data from exchange
|
|
123
131
|
# TODO: check if nbarsback > max_limit (1000) we need to do more requests
|
|
124
|
-
ohlcv_data = await self._exchange_manager.exchange.fetch_ohlcv(
|
|
132
|
+
ohlcv_data = await self._exchange_manager.exchange.fetch_ohlcv(
|
|
133
|
+
ccxt_symbol, exch_timeframe, since=since, limit=nbarsback + 1
|
|
134
|
+
)
|
|
125
135
|
|
|
126
136
|
# Convert to Bar objects using utility method
|
|
127
137
|
bars = []
|
|
@@ -153,7 +163,9 @@ class OhlcDataHandler(BaseDataTypeHandler):
|
|
|
153
163
|
|
|
154
164
|
# ohlcv is symbol -> timeframe -> list[timestamp, open, high, low, close, volume]
|
|
155
165
|
for exch_symbol, _data in ohlcv.items():
|
|
156
|
-
instrument = ccxt_find_instrument(
|
|
166
|
+
instrument = ccxt_find_instrument(
|
|
167
|
+
exch_symbol, self._exchange_manager.exchange, _symbol_to_instrument
|
|
168
|
+
)
|
|
157
169
|
for _, ohlcvs in _data.items():
|
|
158
170
|
for oh in ohlcvs:
|
|
159
171
|
# Use private processing method to avoid duplication
|
|
@@ -261,7 +273,9 @@ class OhlcDataHandler(BaseDataTypeHandler):
|
|
|
261
273
|
individual_subscribers[instrument] = create_individual_subscriber()
|
|
262
274
|
|
|
263
275
|
# Create individual unsubscriber if exchange supports it
|
|
264
|
-
if hasattr(self._exchange_manager.exchange, "un_watch_ohlcv") and callable(
|
|
276
|
+
if hasattr(self._exchange_manager.exchange, "un_watch_ohlcv") and callable(
|
|
277
|
+
getattr(self._exchange_manager.exchange, "un_watch_ohlcv", None)
|
|
278
|
+
):
|
|
265
279
|
|
|
266
280
|
def create_individual_unsubscriber(symbol=ccxt_symbol, exchange_id=self._exchange_id):
|
|
267
281
|
async def individual_unsubscriber():
|
|
@@ -314,7 +328,7 @@ class OhlcDataHandler(BaseDataTypeHandler):
|
|
|
314
328
|
# Use current time for health monitoring with robust conversion
|
|
315
329
|
current_timestamp_ms = current_timestamp_ns // 1_000_000
|
|
316
330
|
health_timestamp = pd.Timestamp(current_timestamp_ms, unit="ms").asm8
|
|
317
|
-
|
|
331
|
+
|
|
318
332
|
# Notify all listeners
|
|
319
333
|
self._data_provider.notify_data_arrival(sub_type, health_timestamp)
|
|
320
334
|
|
|
@@ -329,10 +343,19 @@ class OhlcDataHandler(BaseDataTypeHandler):
|
|
|
329
343
|
# Use provided OHLCV data or fall back to current bar
|
|
330
344
|
quote_data = ohlcv_data_for_quotes or [oh]
|
|
331
345
|
if quote_data:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
346
|
+
self._update_quote(instrument, quote_data)
|
|
347
|
+
|
|
348
|
+
def _update_quote(self, instrument: Instrument, quote_data: list):
|
|
349
|
+
quote = self._convert_ohlcv_to_quote(quote_data, instrument)
|
|
350
|
+
self._data_provider._last_quotes[instrument] = quote
|
|
351
|
+
|
|
352
|
+
def _convert_ohlcv_to_quote(self, oh: list, instrument: Instrument) -> Quote:
|
|
353
|
+
current_time = self._data_provider.time_provider.time()
|
|
354
|
+
current_timestamp_ns = current_time.astype("datetime64[ns]").view("int64")
|
|
355
|
+
_price = oh[-1][4] # Close price
|
|
356
|
+
_s2 = instrument.tick_size / 2.0
|
|
357
|
+
_bid, _ask = _price - _s2, _price + _s2
|
|
358
|
+
return Quote(current_timestamp_ns, _bid, _ask, 0.0, 0.0)
|
|
336
359
|
|
|
337
360
|
def _convert_ohlcv_to_bar(self, oh: list) -> Bar:
|
|
338
361
|
"""
|
|
@@ -452,7 +452,7 @@ class CcxtDataReader(DataReader):
|
|
|
452
452
|
|
|
453
453
|
# Use exchange-specific funding interval (lookup from cache or default)
|
|
454
454
|
exchange_caps = self._capabilities.get(exchange.lower(), ReaderCapabilities())
|
|
455
|
-
funding_interval_hours = self.
|
|
455
|
+
funding_interval_hours = self._get_funding_interval_hours_for_symbol(
|
|
456
456
|
exchange.upper(), ccxt_symbol, exchange_caps.default_funding_interval_hours
|
|
457
457
|
)
|
|
458
458
|
|
|
@@ -602,7 +602,7 @@ class CcxtDataReader(DataReader):
|
|
|
602
602
|
|
|
603
603
|
# Use exchange-specific funding interval (lookup from cache or default)
|
|
604
604
|
exchange_caps = self._capabilities.get(exchange.lower(), ReaderCapabilities())
|
|
605
|
-
funding_interval_hours = self.
|
|
605
|
+
funding_interval_hours = self._get_funding_interval_hours_for_symbol(
|
|
606
606
|
exchange.upper(), ccxt_symbol, exchange_caps.default_funding_interval_hours
|
|
607
607
|
)
|
|
608
608
|
|
|
@@ -652,12 +652,17 @@ class CcxtDataReader(DataReader):
|
|
|
652
652
|
self._funding_intervals_cache[exchange_name] = intervals
|
|
653
653
|
return intervals
|
|
654
654
|
|
|
655
|
-
def
|
|
655
|
+
def _get_funding_interval_hours_for_symbol(
|
|
656
|
+
self, exchange_name: str, ccxt_symbol: str, default_hours: float
|
|
657
|
+
) -> float:
|
|
656
658
|
"""
|
|
657
659
|
Get funding interval for a specific symbol, with exchange-specific lookup and fallback.
|
|
658
660
|
"""
|
|
659
661
|
intervals_dict = self._get_funding_intervals_for_exchange(exchange_name)
|
|
660
|
-
|
|
662
|
+
interval = intervals_dict.get(ccxt_symbol, default_hours)
|
|
663
|
+
if isinstance(interval, str):
|
|
664
|
+
return pd.Timedelta(interval).total_seconds() / 3600
|
|
665
|
+
return float(interval)
|
|
661
666
|
|
|
662
667
|
def _get_column_names(self, data_type: str) -> list[str]:
|
|
663
668
|
match data_type:
|
|
@@ -110,7 +110,19 @@ class MarketManager(IMarketManager):
|
|
|
110
110
|
|
|
111
111
|
def quote(self, instrument: Instrument) -> Quote | None:
|
|
112
112
|
_data_provider = self._get_data_provider(instrument.exchange)
|
|
113
|
-
|
|
113
|
+
quote = _data_provider.get_quote(instrument)
|
|
114
|
+
if quote is None:
|
|
115
|
+
ohlcv = self._cache.get_ohlcv(instrument)
|
|
116
|
+
if len(ohlcv) > 0:
|
|
117
|
+
last_bar = ohlcv[0]
|
|
118
|
+
quote = Quote(
|
|
119
|
+
last_bar.time,
|
|
120
|
+
last_bar.close - instrument.tick_size / 2,
|
|
121
|
+
last_bar.close + instrument.tick_size / 2,
|
|
122
|
+
0,
|
|
123
|
+
0,
|
|
124
|
+
)
|
|
125
|
+
return quote
|
|
114
126
|
|
|
115
127
|
def get_data(self, instrument: Instrument, sub_type: str) -> list[Any]:
|
|
116
128
|
return self._cache.get_data(instrument, sub_type)
|
|
@@ -393,13 +393,13 @@ class ProcessingManager(IProcessingManager):
|
|
|
393
393
|
else:
|
|
394
394
|
_init_signals.append(signal)
|
|
395
395
|
self._instruments_in_init_stage.add(instr)
|
|
396
|
-
logger.
|
|
396
|
+
logger.debug(f"Switching tracker for <g>{instr}</g> to post-warmup initialization")
|
|
397
397
|
else:
|
|
398
398
|
_std_signals.append(signal)
|
|
399
399
|
if instr in self._instruments_in_init_stage:
|
|
400
400
|
_cancel_init_stage_instruments_tracker.add(instr)
|
|
401
401
|
self._instruments_in_init_stage.remove(instr)
|
|
402
|
-
logger.
|
|
402
|
+
logger.debug(f"Switching tracker for <g>{instr}</g> back to defined position tracker")
|
|
403
403
|
|
|
404
404
|
return _std_signals, _init_signals, _cancel_init_stage_instruments_tracker
|
|
405
405
|
|
|
@@ -643,6 +643,8 @@ class ProcessingManager(IProcessingManager):
|
|
|
643
643
|
# - update tracker
|
|
644
644
|
_targets_from_tracker = self._get_tracker_for(instrument).update(self._context, instrument, _update)
|
|
645
645
|
|
|
646
|
+
# TODO: add gatherer update
|
|
647
|
+
|
|
646
648
|
# - notify position gatherer for the new target positions
|
|
647
649
|
if _targets_from_tracker:
|
|
648
650
|
# - tracker generated new targets on update, notify position gatherer
|
|
@@ -14,10 +14,10 @@ class IncrementalFormatter(DefaultFormatter):
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
def __init__(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
self,
|
|
18
|
+
alert_name: str,
|
|
19
|
+
exchange_mapping: Optional[Dict[str, str]] = None,
|
|
20
|
+
account: Optional[IAccountViewer] = None,
|
|
21
21
|
):
|
|
22
22
|
"""
|
|
23
23
|
Initialize the IncrementalFormatter.
|
|
@@ -77,7 +77,7 @@ class IncrementalFormatter(DefaultFormatter):
|
|
|
77
77
|
side = "BUY" if current_leverage > 0 else "SELL"
|
|
78
78
|
return {
|
|
79
79
|
"type": "ENTRY",
|
|
80
|
-
"data": f"{{'action':'ENTRY','exchange':'{exchange}','alertName':'{self.alert_name}','symbol':'{instrument.exchange_symbol}','side':'{side}','leverage':{abs(current_leverage)},'entryPrice':{price}}}",
|
|
80
|
+
"data": f"{{'action':'ENTRY','exchange':'{exchange}','alertName':'{self.alert_name}','symbol':'{instrument.exchange_symbol.upper()}','side':'{side}','leverage':{abs(current_leverage)},'entryPrice':{price}}}",
|
|
81
81
|
}
|
|
82
82
|
else:
|
|
83
83
|
# Same side - generate entry signal with the leverage difference
|
|
@@ -85,7 +85,7 @@ class IncrementalFormatter(DefaultFormatter):
|
|
|
85
85
|
side = "BUY" if current_leverage > 0 else "SELL"
|
|
86
86
|
return {
|
|
87
87
|
"type": "ENTRY",
|
|
88
|
-
"data": f"{{'action':'ENTRY','exchange':'{exchange}','alertName':'{self.alert_name}','symbol':'{instrument.exchange_symbol}','side':'{side}','leverage':{leverage_change},'entryPrice':{price}}}",
|
|
88
|
+
"data": f"{{'action':'ENTRY','exchange':'{exchange}','alertName':'{self.alert_name}','symbol':'{instrument.exchange_symbol.upper()}','side':'{side}','leverage':{leverage_change},'entryPrice':{price}}}",
|
|
89
89
|
}
|
|
90
90
|
else:
|
|
91
91
|
# Position decrease (exit)
|
|
@@ -97,7 +97,7 @@ class IncrementalFormatter(DefaultFormatter):
|
|
|
97
97
|
side = "BUY" if current_leverage > 0 else "SELL"
|
|
98
98
|
return {
|
|
99
99
|
"type": "ENTRY",
|
|
100
|
-
"data": f"{{'action':'ENTRY','exchange':'{exchange}','alertName':'{self.alert_name}','symbol':'{instrument.exchange_symbol}','side':'{side}','leverage':{abs(current_leverage)},'entryPrice':{price}}}",
|
|
100
|
+
"data": f"{{'action':'ENTRY','exchange':'{exchange}','alertName':'{self.alert_name}','symbol':'{instrument.exchange_symbol.upper()}','side':'{side}','leverage':{abs(current_leverage)},'entryPrice':{price}}}",
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
# Calculate the fraction of the position that was closed
|
|
@@ -108,5 +108,5 @@ class IncrementalFormatter(DefaultFormatter):
|
|
|
108
108
|
|
|
109
109
|
return {
|
|
110
110
|
"type": "EXIT",
|
|
111
|
-
"data": f"{{'action':'EXIT','exchange':'{exchange}','alertName':'{self.alert_name}','symbol':'{instrument.exchange_symbol}','exitFraction':{exit_fraction},'exitPrice':{price}}}",
|
|
111
|
+
"data": f"{{'action':'EXIT','exchange':'{exchange}','alertName':'{self.alert_name}','symbol':'{instrument.exchange_symbol.upper()}','exitFraction':{exit_fraction},'exitPrice':{price}}}",
|
|
112
112
|
}
|
|
@@ -157,3 +157,12 @@ cdef class Swings(IndicatorOHLC):
|
|
|
157
157
|
cdef public TimeSeries middles, deltas
|
|
158
158
|
|
|
159
159
|
cpdef double calculate(self, long long time, Bar bar, short new_item_started)
|
|
160
|
+
|
|
161
|
+
cdef class Pivots(IndicatorOHLC):
|
|
162
|
+
cdef int before, after
|
|
163
|
+
cdef object bars_buffer
|
|
164
|
+
cdef Bar current_bar
|
|
165
|
+
cdef long long current_bar_time
|
|
166
|
+
cdef public TimeSeries tops, bottoms, tops_detection_lag, bottoms_detection_lag
|
|
167
|
+
|
|
168
|
+
cpdef double calculate(self, long long time, Bar bar, short new_item_started)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import pandas as pd
|
|
1
2
|
from qubx.core.series import OHLCV, Indicator, IndicatorOHLC, TimeSeries
|
|
2
3
|
|
|
3
4
|
def sma(series: TimeSeries, period: int): ...
|
|
@@ -16,6 +17,7 @@ def psar(series: OHLCV, iaf: float = 0.02, maxaf: float = 0.2): ...
|
|
|
16
17
|
def smooth(series: TimeSeries, smoother: str, *args, **kwargs) -> Indicator: ...
|
|
17
18
|
def atr(series: OHLCV, period: int = 14, smoother="sma", percentage: bool = False): ...
|
|
18
19
|
def swings(series: OHLCV, trend_indicator, **indicator_args) -> Indicator: ...
|
|
20
|
+
def pivots(series: OHLCV, before: int = 5, after: int = 5) -> Indicator: ...
|
|
19
21
|
|
|
20
22
|
class Sma(Indicator):
|
|
21
23
|
def __init__(self, name: str, series: TimeSeries, period: int): ...
|
|
@@ -45,3 +47,11 @@ class Swings(IndicatorOHLC):
|
|
|
45
47
|
bottoms: TimeSeries
|
|
46
48
|
middles: TimeSeries
|
|
47
49
|
deltas: TimeSeries
|
|
50
|
+
|
|
51
|
+
class Pivots(IndicatorOHLC):
|
|
52
|
+
tops: TimeSeries
|
|
53
|
+
bottoms: TimeSeries
|
|
54
|
+
tops_detection_lag: TimeSeries
|
|
55
|
+
bottoms_detection_lag: TimeSeries
|
|
56
|
+
def __init__(self, name: str, series: OHLCV, before: int, after: int): ...
|
|
57
|
+
def pd(self) -> pd.DataFrame: ...
|
|
@@ -858,4 +858,156 @@ def swings(series: OHLCV, trend_indicator, **indicator_args):
|
|
|
858
858
|
"""
|
|
859
859
|
if not isinstance(series, OHLCV):
|
|
860
860
|
raise ValueError("Series must be OHLCV !")
|
|
861
|
-
return Swings.wrap(series, trend_indicator, **indicator_args)
|
|
861
|
+
return Swings.wrap(series, trend_indicator, **indicator_args)
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
cdef class Pivots(IndicatorOHLC):
|
|
865
|
+
"""
|
|
866
|
+
Pivot points detector that identifies local highs and lows using
|
|
867
|
+
lookback (before) and lookahead (after) windows.
|
|
868
|
+
"""
|
|
869
|
+
|
|
870
|
+
def __init__(self, str name, OHLCV series, int before, int after):
|
|
871
|
+
self.before = before
|
|
872
|
+
self.after = after
|
|
873
|
+
|
|
874
|
+
# Deque to store completed bars for pivot detection
|
|
875
|
+
self.bars_buffer = deque(maxlen=before + after + 1)
|
|
876
|
+
|
|
877
|
+
# Keep track of the current unfinished bar separately
|
|
878
|
+
self.current_bar = None
|
|
879
|
+
self.current_bar_time = 0
|
|
880
|
+
|
|
881
|
+
# TimeSeries for pivot points
|
|
882
|
+
self.tops = TimeSeries("tops", series.timeframe, series.max_series_length)
|
|
883
|
+
self.bottoms = TimeSeries("bottoms", series.timeframe, series.max_series_length)
|
|
884
|
+
self.tops_detection_lag = TimeSeries("tops_lag", series.timeframe, series.max_series_length)
|
|
885
|
+
self.bottoms_detection_lag = TimeSeries("bottoms_lag", series.timeframe, series.max_series_length)
|
|
886
|
+
|
|
887
|
+
super().__init__(name, series)
|
|
888
|
+
|
|
889
|
+
cpdef double calculate(self, long long time, Bar bar, short new_item_started):
|
|
890
|
+
cdef int pivot_idx, i
|
|
891
|
+
cdef double pivot_high, pivot_low
|
|
892
|
+
cdef long long pivot_time
|
|
893
|
+
cdef short is_pivot_high, is_pivot_low
|
|
894
|
+
|
|
895
|
+
if new_item_started:
|
|
896
|
+
# If we have a previous bar that was being updated, add it to the buffer as completed
|
|
897
|
+
if self.current_bar is not None and self.current_bar_time > 0:
|
|
898
|
+
self.bars_buffer.append((self.current_bar_time, self.current_bar))
|
|
899
|
+
|
|
900
|
+
# Start tracking the new unfinished bar
|
|
901
|
+
self.current_bar = bar
|
|
902
|
+
self.current_bar_time = time
|
|
903
|
+
|
|
904
|
+
# Check if we have enough completed bars to detect a pivot
|
|
905
|
+
# We need exactly before + after + 1 bars in the buffer
|
|
906
|
+
if len(self.bars_buffer) < self.before + self.after + 1:
|
|
907
|
+
return np.nan
|
|
908
|
+
|
|
909
|
+
# The pivot candidate is at index 'before'
|
|
910
|
+
pivot_idx = self.before
|
|
911
|
+
pivot_time = self.bars_buffer[pivot_idx][0]
|
|
912
|
+
pivot_bar = self.bars_buffer[pivot_idx][1]
|
|
913
|
+
pivot_high = pivot_bar.high
|
|
914
|
+
pivot_low = pivot_bar.low
|
|
915
|
+
|
|
916
|
+
# Check for pivot high: pivot high must be > all other highs in window
|
|
917
|
+
is_pivot_high = 1
|
|
918
|
+
for i in range(len(self.bars_buffer)):
|
|
919
|
+
if i != pivot_idx:
|
|
920
|
+
if self.bars_buffer[i][1].high >= pivot_high:
|
|
921
|
+
is_pivot_high = 0
|
|
922
|
+
break
|
|
923
|
+
|
|
924
|
+
# Check for pivot low: pivot low must be < all other lows in window
|
|
925
|
+
is_pivot_low = 1
|
|
926
|
+
for i in range(len(self.bars_buffer)):
|
|
927
|
+
if i != pivot_idx:
|
|
928
|
+
if self.bars_buffer[i][1].low <= pivot_low:
|
|
929
|
+
is_pivot_low = 0
|
|
930
|
+
break
|
|
931
|
+
|
|
932
|
+
# Record pivot high if found
|
|
933
|
+
if is_pivot_high:
|
|
934
|
+
self.tops.update(pivot_time, pivot_high)
|
|
935
|
+
# Detection time is now (when we actually detect it)
|
|
936
|
+
self.tops_detection_lag.update(pivot_time, time - pivot_time)
|
|
937
|
+
|
|
938
|
+
# Record pivot low if found
|
|
939
|
+
if is_pivot_low:
|
|
940
|
+
self.bottoms.update(pivot_time, pivot_low)
|
|
941
|
+
# Detection time is now (when we actually detect it)
|
|
942
|
+
self.bottoms_detection_lag.update(pivot_time, time - pivot_time)
|
|
943
|
+
|
|
944
|
+
# Return 1 for pivot high, -1 for pivot low, 0 for both, nan for neither
|
|
945
|
+
if is_pivot_high and is_pivot_low:
|
|
946
|
+
return 0
|
|
947
|
+
elif is_pivot_high:
|
|
948
|
+
return 1
|
|
949
|
+
elif is_pivot_low:
|
|
950
|
+
return -1
|
|
951
|
+
else:
|
|
952
|
+
return np.nan
|
|
953
|
+
else:
|
|
954
|
+
# Just update the current unfinished bar
|
|
955
|
+
self.current_bar = bar
|
|
956
|
+
# Note: current_bar_time stays the same since we're updating the same bar
|
|
957
|
+
return np.nan
|
|
958
|
+
|
|
959
|
+
def pd(self) -> pd.DataFrame:
|
|
960
|
+
"""
|
|
961
|
+
Return DataFrame with pivot points and detection lags.
|
|
962
|
+
|
|
963
|
+
Returns a multi-column DataFrame with:
|
|
964
|
+
- Tops: price, detection_lag, spotted (time when pivot was detected)
|
|
965
|
+
- Bottoms: price, detection_lag, spotted
|
|
966
|
+
"""
|
|
967
|
+
from qubx.pandaz.utils import scols
|
|
968
|
+
|
|
969
|
+
tps = self.tops.pd()
|
|
970
|
+
bts = self.bottoms.pd()
|
|
971
|
+
tpl = self.tops_detection_lag.pd()
|
|
972
|
+
btl = self.bottoms_detection_lag.pd()
|
|
973
|
+
|
|
974
|
+
# Convert lags to timedeltas
|
|
975
|
+
if len(tpl) > 0:
|
|
976
|
+
tpl = tpl.apply(lambda x: pd.Timedelta(x, unit='ns'))
|
|
977
|
+
if len(btl) > 0:
|
|
978
|
+
btl = btl.apply(lambda x: pd.Timedelta(x, unit='ns'))
|
|
979
|
+
|
|
980
|
+
# Create DataFrames for tops and bottoms
|
|
981
|
+
if len(tps) > 0:
|
|
982
|
+
tops_df = pd.DataFrame({
|
|
983
|
+
'price': tps,
|
|
984
|
+
'detection_lag': tpl,
|
|
985
|
+
'spotted': pd.Series(tps.index + tpl.values, index=tps.index)
|
|
986
|
+
})
|
|
987
|
+
else:
|
|
988
|
+
tops_df = pd.DataFrame(columns=['price', 'detection_lag', 'spotted'])
|
|
989
|
+
|
|
990
|
+
if len(bts) > 0:
|
|
991
|
+
bottoms_df = pd.DataFrame({
|
|
992
|
+
'price': bts,
|
|
993
|
+
'detection_lag': btl,
|
|
994
|
+
'spotted': pd.Series(bts.index + btl.values, index=bts.index)
|
|
995
|
+
})
|
|
996
|
+
else:
|
|
997
|
+
bottoms_df = pd.DataFrame(columns=['price', 'detection_lag', 'spotted'])
|
|
998
|
+
|
|
999
|
+
return scols(tops_df, bottoms_df, keys=["Tops", "Bottoms"])
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def pivots(series: OHLCV, before: int = 5, after: int = 5):
|
|
1003
|
+
"""
|
|
1004
|
+
Pivot points detector using lookback/lookahead windows.
|
|
1005
|
+
|
|
1006
|
+
:param series: OHLCV series
|
|
1007
|
+
:param before: Number of bars to look back
|
|
1008
|
+
:param after: Number of bars to look ahead
|
|
1009
|
+
:return: Pivots indicator with tops and bottoms
|
|
1010
|
+
"""
|
|
1011
|
+
if not isinstance(series, OHLCV):
|
|
1012
|
+
raise ValueError("Series must be OHLCV!")
|
|
1013
|
+
return Pivots.wrap(series, before, after)
|
|
@@ -989,7 +989,7 @@ class _InitializationStageTracker(GenericRiskControllerDecorator, IPositionSizer
|
|
|
989
989
|
continue
|
|
990
990
|
|
|
991
991
|
_current_pos = ctx.get_position(s.instrument).quantity
|
|
992
|
-
logger.
|
|
992
|
+
logger.debug(
|
|
993
993
|
f"[<y>{self.__class__.__name__}</y>] :: <y>Processing init signal</y> :: {s} :: Position is {_current_pos}"
|
|
994
994
|
)
|
|
995
995
|
_to_proceed.append(s)
|
|
@@ -1183,13 +1183,25 @@ def plot_trends(trends: pd.DataFrame | Struct, uc="w--", dc="m--", lw=2, ms=6, f
|
|
|
1183
1183
|
raise ValueError("trends must be a DataFrame or Struct with 'trends' attribute")
|
|
1184
1184
|
|
|
1185
1185
|
|
|
1186
|
-
def plot_quantiles(
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1186
|
+
def plot_quantiles(
|
|
1187
|
+
data,
|
|
1188
|
+
top_n=10,
|
|
1189
|
+
bottom_n=10,
|
|
1190
|
+
title=None,
|
|
1191
|
+
ylabel=None,
|
|
1192
|
+
xlabel="Items",
|
|
1193
|
+
figsize=(16, 10),
|
|
1194
|
+
positive_color="#2E8B57",
|
|
1195
|
+
negative_color="#DC143C",
|
|
1196
|
+
value_formatter=None,
|
|
1197
|
+
label_transformer=None,
|
|
1198
|
+
zero_line=True,
|
|
1199
|
+
positive_label=None,
|
|
1200
|
+
negative_label=None,
|
|
1201
|
+
):
|
|
1190
1202
|
"""
|
|
1191
1203
|
Plot top and bottom quantiles of data as a bar chart with customizable styling.
|
|
1192
|
-
|
|
1204
|
+
|
|
1193
1205
|
Parameters:
|
|
1194
1206
|
-----------
|
|
1195
1207
|
data : pd.Series
|
|
@@ -1214,28 +1226,26 @@ def plot_quantiles(data, top_n=10, bottom_n=10, title=None, ylabel=None, xlabel=
|
|
|
1214
1226
|
Function to format values for display (e.g., lambda x: f'{x:.1f}%')
|
|
1215
1227
|
label_transformer : callable, optional
|
|
1216
1228
|
Function to transform index labels (e.g., lambda x: x.replace('USDC', ''))
|
|
1217
|
-
show_values : bool, default True
|
|
1218
|
-
Whether to show values on bars
|
|
1219
1229
|
zero_line : bool, default True
|
|
1220
1230
|
Whether to show horizontal line at zero
|
|
1221
1231
|
positive_label : str, optional
|
|
1222
1232
|
Legend label for positive values
|
|
1223
1233
|
negative_label : str, optional
|
|
1224
1234
|
Legend label for negative values
|
|
1225
|
-
|
|
1235
|
+
|
|
1226
1236
|
Returns:
|
|
1227
1237
|
--------
|
|
1228
1238
|
fig, ax : matplotlib figure and axes objects
|
|
1229
|
-
|
|
1239
|
+
|
|
1230
1240
|
Examples:
|
|
1231
1241
|
---------
|
|
1232
1242
|
# Basic usage
|
|
1233
1243
|
plot_quantiles(data_series)
|
|
1234
|
-
|
|
1244
|
+
|
|
1235
1245
|
# Funding rates example
|
|
1236
|
-
plot_quantiles(funding_rates,
|
|
1246
|
+
plot_quantiles(funding_rates,
|
|
1237
1247
|
title="Annualized Funding Rates",
|
|
1238
|
-
ylabel="Rate (%)",
|
|
1248
|
+
ylabel="Rate (%)",
|
|
1239
1249
|
value_formatter=lambda x: f'{x:.1f}%',
|
|
1240
1250
|
label_transformer=lambda x: x.replace('USDC', ''),
|
|
1241
1251
|
positive_label="Longs pay Shorts",
|
|
@@ -1243,65 +1253,49 @@ def plot_quantiles(data, top_n=10, bottom_n=10, title=None, ylabel=None, xlabel=
|
|
|
1243
1253
|
"""
|
|
1244
1254
|
import pandas as pd
|
|
1245
1255
|
from matplotlib.patches import Patch
|
|
1246
|
-
|
|
1256
|
+
|
|
1247
1257
|
# Get top and bottom quantiles
|
|
1248
1258
|
top_data = data.head(top_n)
|
|
1249
1259
|
bottom_data = data.tail(bottom_n)
|
|
1250
|
-
|
|
1260
|
+
|
|
1251
1261
|
# Combine them
|
|
1252
1262
|
combined_data = pd.concat([top_data, bottom_data])
|
|
1253
|
-
|
|
1263
|
+
|
|
1254
1264
|
# Create the plot
|
|
1255
1265
|
fig, ax = plt.subplots(figsize=figsize)
|
|
1256
|
-
|
|
1266
|
+
|
|
1257
1267
|
# Create colors: positive_color for positive, negative_color for negative
|
|
1258
1268
|
colors = [positive_color if value >= 0 else negative_color for value in combined_data.values]
|
|
1259
|
-
|
|
1269
|
+
|
|
1260
1270
|
# Create bar plot
|
|
1261
|
-
bars = ax.bar(
|
|
1262
|
-
|
|
1263
|
-
|
|
1271
|
+
bars = ax.bar(
|
|
1272
|
+
range(len(combined_data)), combined_data.values, color=colors, alpha=0.8, edgecolor="white", linewidth=0.5
|
|
1273
|
+
)
|
|
1274
|
+
|
|
1264
1275
|
# Customize the plot
|
|
1265
1276
|
if title:
|
|
1266
|
-
ax.set_title(title, fontsize=16, fontweight=
|
|
1277
|
+
ax.set_title(title, fontsize=16, fontweight="bold", pad=20)
|
|
1267
1278
|
if ylabel:
|
|
1268
|
-
ax.set_ylabel(ylabel, fontsize=12, fontweight=
|
|
1269
|
-
ax.set_xlabel(xlabel, fontsize=12, fontweight=
|
|
1270
|
-
|
|
1279
|
+
ax.set_ylabel(ylabel, fontsize=12, fontweight="bold")
|
|
1280
|
+
ax.set_xlabel(xlabel, fontsize=12, fontweight="bold")
|
|
1281
|
+
|
|
1271
1282
|
# Set x-axis labels
|
|
1272
1283
|
if label_transformer:
|
|
1273
1284
|
labels = [label_transformer(str(label)) for label in combined_data.index]
|
|
1274
1285
|
else:
|
|
1275
1286
|
labels = [str(label) for label in combined_data.index]
|
|
1276
|
-
|
|
1287
|
+
|
|
1277
1288
|
ax.set_xticks(range(len(combined_data)))
|
|
1278
|
-
ax.set_xticklabels(labels, rotation=45, ha=
|
|
1279
|
-
|
|
1289
|
+
ax.set_xticklabels(labels, rotation=45, ha="right")
|
|
1290
|
+
|
|
1280
1291
|
# Add horizontal line at zero
|
|
1281
1292
|
if zero_line:
|
|
1282
|
-
ax.axhline(y=0, color=
|
|
1283
|
-
|
|
1293
|
+
ax.axhline(y=0, color="black", linestyle="-", alpha=0.4, linewidth=1.5)
|
|
1294
|
+
|
|
1284
1295
|
# Add grid
|
|
1285
|
-
ax.grid(True, alpha=0.25, linestyle=
|
|
1296
|
+
ax.grid(True, alpha=0.25, linestyle=":", linewidth=0.8, color="#666666")
|
|
1286
1297
|
ax.set_axisbelow(True)
|
|
1287
|
-
|
|
1288
|
-
# Add value labels on bars
|
|
1289
|
-
if show_values:
|
|
1290
|
-
if value_formatter is None:
|
|
1291
|
-
value_formatter = lambda x: f'{x:.1f}'
|
|
1292
|
-
|
|
1293
|
-
for bar, value in zip(bars, combined_data.values):
|
|
1294
|
-
height = bar.get_height()
|
|
1295
|
-
label_offset = max(abs(height) * 0.02, 5) # Dynamic offset based on value magnitude
|
|
1296
|
-
|
|
1297
|
-
ax.text(bar.get_x() + bar.get_width()/2.,
|
|
1298
|
-
height + (label_offset if height >= 0 else -label_offset),
|
|
1299
|
-
value_formatter(value),
|
|
1300
|
-
ha='center',
|
|
1301
|
-
va='bottom' if height >= 0 else 'top',
|
|
1302
|
-
fontsize=10, fontweight='bold',
|
|
1303
|
-
color='white' if abs(height) > max(abs(combined_data.min()), abs(combined_data.max())) * 0.3 else 'black')
|
|
1304
|
-
|
|
1298
|
+
|
|
1305
1299
|
# Add legend if labels provided
|
|
1306
1300
|
if positive_label or negative_label:
|
|
1307
1301
|
legend_elements = []
|
|
@@ -1310,8 +1304,8 @@ def plot_quantiles(data, top_n=10, bottom_n=10, title=None, ylabel=None, xlabel=
|
|
|
1310
1304
|
if negative_label:
|
|
1311
1305
|
legend_elements.append(Patch(facecolor=negative_color, label=negative_label))
|
|
1312
1306
|
if legend_elements:
|
|
1313
|
-
ax.legend(handles=legend_elements, loc=
|
|
1314
|
-
|
|
1307
|
+
ax.legend(handles=legend_elements, loc="upper right", fontsize=10, framealpha=0.9)
|
|
1308
|
+
|
|
1315
1309
|
plt.tight_layout()
|
|
1316
|
-
|
|
1310
|
+
|
|
1317
1311
|
return fig, ax
|