Qubx 0.6.62__tar.gz → 0.6.64__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-0.6.62 → qubx-0.6.64}/PKG-INFO +1 -1
- {qubx-0.6.62 → qubx-0.6.64}/pyproject.toml +1 -1
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/data.py +5 -1
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/ome.py +5 -3
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/simulated_data.py +43 -1
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/simulator.py +25 -15
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/utils.py +68 -25
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/account.py +81 -9
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/basics.py +97 -3
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/context.py +8 -3
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/helpers.py +11 -4
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/interfaces.py +36 -2
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/loggers.py +19 -16
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/lookups.py +7 -7
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/metrics.py +42 -4
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/mixins/market.py +22 -12
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/mixins/processing.py +65 -8
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/series.pyi +4 -3
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/series.pyx +34 -12
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/utils.pyx +3 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/data/helpers.py +75 -39
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/data/readers.py +224 -15
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/data/registry.py +1 -1
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/emitters/__init__.py +2 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/emitters/base.py +23 -2
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/emitters/composite.py +17 -2
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/emitters/csv.py +43 -1
- qubx-0.6.64/src/qubx/emitters/inmemory.py +244 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/emitters/prometheus.py +57 -2
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/emitters/questdb.py +131 -2
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/features/core.py +11 -8
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/features/orderbook.py +2 -1
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/features/trades.py +1 -1
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/gathering/simplest.py +7 -1
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/loggers/inmemory.py +28 -13
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/pandaz/ta.py +11 -20
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/pandaz/utils.py +11 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/trackers/riskctrl.py +3 -4
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/time.py +3 -1
- {qubx-0.6.62 → qubx-0.6.64}/LICENSE +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/README.md +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/build.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/account.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/broker.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/management.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/optimization.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/runner.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/cli/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/cli/commands.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/cli/deploy.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/cli/misc.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/cli/release.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/cli/tui.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/reader.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/deque.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/errors.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/exceptions.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/initializer.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/series.pxd +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/data/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/data/composite.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/data/hft.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/data/tardis.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/emitters/indicator.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/exporters/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/exporters/composite.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/exporters/slack.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/features/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/features/price.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/features/utils.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/health/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/health/base.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/loggers/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/loggers/csv.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/loggers/factory.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/loggers/mongo.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/math/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/math/stats.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/notifications/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/notifications/composite.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/notifications/slack.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/notifications/throttler.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/_build.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-hyperliquid-spot.json +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-hyperliquid.f-perpetual.json +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restarts/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restorers/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restorers/balance.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restorers/factory.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restorers/position.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restorers/signal.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restorers/state.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restorers/utils.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/trackers/advanced.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/trackers/sizers.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/collections.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/misc.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/orderbook.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/questdb.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/runner/configs.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/runner/runner.py +0 -0
- {qubx-0.6.62 → qubx-0.6.64}/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.64"
|
|
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"
|
|
@@ -68,6 +68,10 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
68
68
|
|
|
69
69
|
# - provide historical data and last quote for subscribed instruments
|
|
70
70
|
for i in _new_instr:
|
|
71
|
+
# Check if the instrument was actually subscribed (not filtered out)
|
|
72
|
+
if not self.has_subscription(i, subscription_type):
|
|
73
|
+
continue
|
|
74
|
+
|
|
71
75
|
h_data = self._data_source.peek_historical_data(i, subscription_type)
|
|
72
76
|
if h_data:
|
|
73
77
|
# _s_type = DataType.from_str(subscription_type)[0]
|
|
@@ -119,7 +123,7 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
119
123
|
end = start - nbarsback * (_timeframe := pd.Timedelta(timeframe))
|
|
120
124
|
_spec = f"{instrument.exchange}:{instrument.symbol}"
|
|
121
125
|
return self._convert_records_to_bars(
|
|
122
|
-
_reader.read(data_id=_spec, start=start, stop=end, transform=AsDict()), # type: ignore
|
|
126
|
+
_reader.read(data_id=_spec, start=start, stop=end, timeframe=timeframe, transform=AsDict()), # type: ignore
|
|
123
127
|
time_as_nsec(self.time_provider.time()),
|
|
124
128
|
_timeframe.asm8.item(),
|
|
125
129
|
)
|
|
@@ -6,17 +6,17 @@ from sortedcontainers import SortedDict
|
|
|
6
6
|
|
|
7
7
|
from qubx import logger
|
|
8
8
|
from qubx.core.basics import (
|
|
9
|
+
OPTION_AVOID_STOP_ORDER_PRICE_VALIDATION,
|
|
9
10
|
OPTION_FILL_AT_SIGNAL_PRICE,
|
|
10
11
|
OPTION_SIGNAL_PRICE,
|
|
11
12
|
OPTION_SKIP_PRICE_CROSS_CONTROL,
|
|
12
|
-
OPTION_AVOID_STOP_ORDER_PRICE_VALIDATION,
|
|
13
13
|
Deal,
|
|
14
14
|
Instrument,
|
|
15
15
|
ITimeProvider,
|
|
16
16
|
Order,
|
|
17
17
|
OrderSide,
|
|
18
|
-
OrderType,
|
|
19
18
|
OrderStatus,
|
|
19
|
+
OrderType,
|
|
20
20
|
TransactionCostsCalculator,
|
|
21
21
|
dt_64,
|
|
22
22
|
)
|
|
@@ -208,7 +208,9 @@ class OrdersManagementEngine:
|
|
|
208
208
|
**options,
|
|
209
209
|
) -> SimulatedExecutionReport:
|
|
210
210
|
if self.bbo is None:
|
|
211
|
-
raise
|
|
211
|
+
raise SimulationError(
|
|
212
|
+
f"Simulator is not ready for order management - no quote for {self.instrument.symbol}"
|
|
213
|
+
)
|
|
212
214
|
|
|
213
215
|
# - validate order parameters
|
|
214
216
|
self._validate_order(order_side, order_type, amount, price, time_in_force, options)
|
|
@@ -3,11 +3,12 @@ from typing import Any, Iterator
|
|
|
3
3
|
import pandas as pd
|
|
4
4
|
|
|
5
5
|
from qubx import logger
|
|
6
|
-
from qubx.core.basics import DataType, Instrument, Timestamped
|
|
6
|
+
from qubx.core.basics import DataType, Instrument, MarketType, Timestamped
|
|
7
7
|
from qubx.core.exceptions import SimulationError
|
|
8
8
|
from qubx.data.composite import IteratedDataStreamsSlicer
|
|
9
9
|
from qubx.data.readers import (
|
|
10
10
|
AsDict,
|
|
11
|
+
AsFundingPayments,
|
|
11
12
|
AsOrderBook,
|
|
12
13
|
AsQuotes,
|
|
13
14
|
AsTrades,
|
|
@@ -87,6 +88,11 @@ class DataFetcher:
|
|
|
87
88
|
self._producing_data_type = "orderbook"
|
|
88
89
|
self._transformer = AsOrderBook()
|
|
89
90
|
|
|
91
|
+
case DataType.FUNDING_PAYMENT:
|
|
92
|
+
self._requested_data_type = "funding_payment"
|
|
93
|
+
self._producing_data_type = "funding_payment"
|
|
94
|
+
self._transformer = AsFundingPayments()
|
|
95
|
+
|
|
90
96
|
case _:
|
|
91
97
|
self._requested_data_type = subtype
|
|
92
98
|
self._producing_data_type = subtype
|
|
@@ -250,9 +256,45 @@ class IterableSimulationData(Iterator):
|
|
|
250
256
|
_access_key = f"{_subtype}"
|
|
251
257
|
return _access_key, _subtype, _params
|
|
252
258
|
|
|
259
|
+
def _filter_instruments_for_subscription(self, data_type: str, instruments: list[Instrument]) -> list[Instrument]:
|
|
260
|
+
"""
|
|
261
|
+
Filter instruments based on subscription type requirements.
|
|
262
|
+
|
|
263
|
+
For funding payment subscriptions, only SWAP instruments are supported since
|
|
264
|
+
funding payments are specific to perpetual swap contracts.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
data_type: The data type being subscribed to
|
|
268
|
+
instruments: List of instruments to filter
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Filtered list of instruments appropriate for the subscription type
|
|
272
|
+
"""
|
|
273
|
+
# Only funding payments require special filtering
|
|
274
|
+
if data_type == DataType.FUNDING_PAYMENT:
|
|
275
|
+
original_count = len(instruments)
|
|
276
|
+
filtered_instruments = [i for i in instruments if i.market_type == MarketType.SWAP]
|
|
277
|
+
filtered_count = len(filtered_instruments)
|
|
278
|
+
|
|
279
|
+
# Log if instruments were filtered out (debug info)
|
|
280
|
+
if filtered_count < original_count:
|
|
281
|
+
logger.debug(f"Filtered {original_count - filtered_count} non-SWAP instruments from funding payment subscription")
|
|
282
|
+
|
|
283
|
+
return filtered_instruments
|
|
284
|
+
|
|
285
|
+
# For all other subscription types, return instruments unchanged
|
|
286
|
+
return instruments
|
|
287
|
+
|
|
253
288
|
def add_instruments_for_subscription(self, subscription: str, instruments: list[Instrument] | Instrument):
|
|
254
289
|
instruments = instruments if isinstance(instruments, list) else [instruments]
|
|
255
290
|
_subt_key, _data_type, _params = self._parse_subscription_spec(subscription)
|
|
291
|
+
|
|
292
|
+
# Filter instruments based on subscription type requirements
|
|
293
|
+
instruments = self._filter_instruments_for_subscription(_data_type, instruments)
|
|
294
|
+
|
|
295
|
+
# If no instruments remain after filtering, skip subscription
|
|
296
|
+
if not instruments:
|
|
297
|
+
return
|
|
256
298
|
|
|
257
299
|
fetcher = self._subtyped_fetchers.get(_subt_key)
|
|
258
300
|
if not fetcher:
|
|
@@ -4,12 +4,12 @@ import pandas as pd
|
|
|
4
4
|
from joblib import delayed
|
|
5
5
|
|
|
6
6
|
from qubx import QubxLogConfig, logger
|
|
7
|
+
from qubx.core.basics import Instrument
|
|
7
8
|
from qubx.core.exceptions import SimulationError
|
|
8
9
|
from qubx.core.metrics import TradingSessionResult
|
|
9
10
|
from qubx.data.readers import DataReader
|
|
11
|
+
from qubx.emitters.inmemory import InMemoryMetricEmitter
|
|
10
12
|
from qubx.utils.misc import ProgressParallel, Stopwatch, get_current_user
|
|
11
|
-
from qubx.utils.runner.configs import EmissionConfig
|
|
12
|
-
from qubx.utils.runner.factory import create_metric_emitters
|
|
13
13
|
from qubx.utils.time import handle_start_stop
|
|
14
14
|
|
|
15
15
|
from .runner import SimulationRunner
|
|
@@ -31,11 +31,11 @@ def simulate(
|
|
|
31
31
|
strategies: StrategiesDecls_t,
|
|
32
32
|
data: DataDecls_t,
|
|
33
33
|
capital: float | dict[str, float],
|
|
34
|
-
instruments: list[
|
|
34
|
+
instruments: list[str] | list[Instrument] | dict[ExchangeName_t, list[SymbolOrInstrument_t]],
|
|
35
35
|
commissions: str | dict[str, str | None] | None,
|
|
36
36
|
start: str | pd.Timestamp,
|
|
37
37
|
stop: str | pd.Timestamp | None = None,
|
|
38
|
-
exchange: ExchangeName_t | None = None,
|
|
38
|
+
exchange: ExchangeName_t | list[ExchangeName_t] | None = None,
|
|
39
39
|
base_currency: str = "USDT",
|
|
40
40
|
n_jobs: int = 1,
|
|
41
41
|
silent: bool = False,
|
|
@@ -47,7 +47,8 @@ def simulate(
|
|
|
47
47
|
show_latency_report: bool = False,
|
|
48
48
|
portfolio_log_freq: str = "5Min",
|
|
49
49
|
parallel_backend: Literal["loky", "multiprocessing"] = "multiprocessing",
|
|
50
|
-
|
|
50
|
+
enable_inmemory_emitter: bool = False,
|
|
51
|
+
emitter_stats_interval: str = "1h",
|
|
51
52
|
run_separate_instruments: bool = False,
|
|
52
53
|
) -> list[TradingSessionResult]:
|
|
53
54
|
"""
|
|
@@ -73,7 +74,8 @@ def simulate(
|
|
|
73
74
|
- show_latency_report: If True, shows simulator's latency report.
|
|
74
75
|
- portfolio_log_freq (str): Frequency for portfolio logging, default is "5Min".
|
|
75
76
|
- parallel_backend (Literal["loky", "multiprocessing"]): Backend for parallel processing, default is "multiprocessing".
|
|
76
|
-
-
|
|
77
|
+
- enable_inmemory_emitter (bool): If True, attaches an in-memory metric emitter and returns its dataframe in TradingSessionResult.emitter_data.
|
|
78
|
+
- emitter_stats_interval (str): Interval for emitting stats in the in-memory emitter (default: "1h").
|
|
77
79
|
- run_separate_instruments (bool): If True, creates separate simulation setups for each instrument, default is False.
|
|
78
80
|
|
|
79
81
|
Returns:
|
|
@@ -143,7 +145,8 @@ def simulate(
|
|
|
143
145
|
show_latency_report=show_latency_report,
|
|
144
146
|
portfolio_log_freq=portfolio_log_freq,
|
|
145
147
|
parallel_backend=parallel_backend,
|
|
146
|
-
|
|
148
|
+
enable_inmemory_emitter=enable_inmemory_emitter,
|
|
149
|
+
emitter_stats_interval=emitter_stats_interval,
|
|
147
150
|
)
|
|
148
151
|
|
|
149
152
|
|
|
@@ -157,7 +160,8 @@ def _run_setups(
|
|
|
157
160
|
show_latency_report: bool = False,
|
|
158
161
|
portfolio_log_freq: str = "5Min",
|
|
159
162
|
parallel_backend: Literal["loky", "multiprocessing"] = "multiprocessing",
|
|
160
|
-
|
|
163
|
+
enable_inmemory_emitter: bool = False,
|
|
164
|
+
emitter_stats_interval: str = "1h",
|
|
161
165
|
) -> list[TradingSessionResult]:
|
|
162
166
|
# loggers don't work well with joblib and multiprocessing in general because they contain
|
|
163
167
|
# open file handlers that cannot be pickled. I found a solution which requires the usage of enqueue=True
|
|
@@ -179,7 +183,8 @@ def _run_setups(
|
|
|
179
183
|
silent,
|
|
180
184
|
show_latency_report,
|
|
181
185
|
portfolio_log_freq,
|
|
182
|
-
|
|
186
|
+
enable_inmemory_emitter,
|
|
187
|
+
emitter_stats_interval,
|
|
183
188
|
)
|
|
184
189
|
for id, setup in enumerate(strategies_setups)
|
|
185
190
|
]
|
|
@@ -197,7 +202,8 @@ def _run_setups(
|
|
|
197
202
|
silent,
|
|
198
203
|
show_latency_report,
|
|
199
204
|
portfolio_log_freq,
|
|
200
|
-
|
|
205
|
+
enable_inmemory_emitter,
|
|
206
|
+
emitter_stats_interval,
|
|
201
207
|
)
|
|
202
208
|
for id, setup in enumerate(strategies_setups)
|
|
203
209
|
)
|
|
@@ -223,14 +229,14 @@ def _run_setup(
|
|
|
223
229
|
silent: bool,
|
|
224
230
|
show_latency_report: bool,
|
|
225
231
|
portfolio_log_freq: str,
|
|
226
|
-
|
|
232
|
+
enable_inmemory_emitter: bool = False,
|
|
233
|
+
emitter_stats_interval: str = "1h",
|
|
227
234
|
) -> TradingSessionResult | None:
|
|
228
235
|
try:
|
|
229
|
-
# Create metric emitter if configured
|
|
230
236
|
emitter = None
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
237
|
+
emitter_data = None
|
|
238
|
+
if enable_inmemory_emitter:
|
|
239
|
+
emitter = InMemoryMetricEmitter(stats_interval=emitter_stats_interval)
|
|
234
240
|
runner = SimulationRunner(
|
|
235
241
|
setup=setup,
|
|
236
242
|
data_config=data_setup,
|
|
@@ -258,6 +264,9 @@ def _run_setup(
|
|
|
258
264
|
# Filter out None values to match TradingSessionResult expected type
|
|
259
265
|
commissions_for_result = {k: v for k, v in commissions_for_result.items() if v is not None}
|
|
260
266
|
|
|
267
|
+
if enable_inmemory_emitter and emitter is not None:
|
|
268
|
+
emitter_data = emitter.get_dataframe()
|
|
269
|
+
|
|
261
270
|
return TradingSessionResult(
|
|
262
271
|
setup_id,
|
|
263
272
|
setup.name,
|
|
@@ -276,6 +285,7 @@ def _run_setup(
|
|
|
276
285
|
parameters=runner.strategy_params,
|
|
277
286
|
is_simulation=True,
|
|
278
287
|
author=get_current_user(),
|
|
288
|
+
emitter_data=emitter_data,
|
|
279
289
|
)
|
|
280
290
|
except Exception as e:
|
|
281
291
|
logger.error(f"Simulation setup {setup_id} failed with error: {e}")
|
|
@@ -213,38 +213,81 @@ class SignalsProxy(IStrategy):
|
|
|
213
213
|
return None
|
|
214
214
|
|
|
215
215
|
|
|
216
|
+
def _process_single_symbol_or_instrument(
|
|
217
|
+
symbol_or_instrument: SymbolOrInstrument_t,
|
|
218
|
+
default_exchange: ExchangeName_t | None,
|
|
219
|
+
requested_exchange: ExchangeName_t | None,
|
|
220
|
+
) -> tuple[Instrument | None, str | None]:
|
|
221
|
+
"""
|
|
222
|
+
Process a single symbol or instrument and return the resolved instrument and exchange.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
tuple[Instrument | None, str | None]: (instrument, exchange) or (None, None) if processing failed
|
|
226
|
+
"""
|
|
227
|
+
match symbol_or_instrument:
|
|
228
|
+
case str():
|
|
229
|
+
_e, _s = (
|
|
230
|
+
symbol_or_instrument.split(":")
|
|
231
|
+
if ":" in symbol_or_instrument
|
|
232
|
+
else (default_exchange, symbol_or_instrument)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if _e is None:
|
|
236
|
+
logger.warning(
|
|
237
|
+
f"Can't extract exchange name from symbol's spec ({symbol_or_instrument}) and exact exchange name is not provided - skip this symbol !"
|
|
238
|
+
)
|
|
239
|
+
return None, None
|
|
240
|
+
|
|
241
|
+
if (
|
|
242
|
+
requested_exchange is not None
|
|
243
|
+
and isinstance(requested_exchange, str)
|
|
244
|
+
and _e.lower() != requested_exchange.lower()
|
|
245
|
+
):
|
|
246
|
+
logger.warning(
|
|
247
|
+
f"Exchange from symbol's spec ({_e}) is different from requested: {requested_exchange} !"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if (instrument := lookup.find_symbol(_e, _s)) is not None:
|
|
251
|
+
return instrument, _e.upper()
|
|
252
|
+
else:
|
|
253
|
+
logger.warning(f"Can't find instrument for specified symbol ({symbol_or_instrument}) - ignoring !")
|
|
254
|
+
return None, None
|
|
255
|
+
|
|
256
|
+
case Instrument():
|
|
257
|
+
return symbol_or_instrument, symbol_or_instrument.exchange
|
|
258
|
+
|
|
259
|
+
case _:
|
|
260
|
+
raise SimulationConfigError(
|
|
261
|
+
f"Unsupported type for {symbol_or_instrument} only str or Instrument instances are allowed!"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
216
265
|
def find_instruments_and_exchanges(
|
|
217
266
|
instruments: list[SymbolOrInstrument_t] | dict[ExchangeName_t, list[SymbolOrInstrument_t]],
|
|
218
|
-
exchange: ExchangeName_t | None,
|
|
267
|
+
exchange: ExchangeName_t | list[ExchangeName_t] | None,
|
|
219
268
|
) -> tuple[list[Instrument], list[ExchangeName_t]]:
|
|
220
269
|
_instrs: list[Instrument] = []
|
|
221
|
-
_exchanges = [] if exchange is None else [exchange]
|
|
222
|
-
for i in instruments:
|
|
223
|
-
match i:
|
|
224
|
-
case str():
|
|
225
|
-
_e, _s = i.split(":") if ":" in i else (exchange, i)
|
|
226
|
-
assert _e is not None
|
|
270
|
+
_exchanges = [] if exchange is None else [exchange] if isinstance(exchange, str) else exchange
|
|
227
271
|
|
|
228
|
-
|
|
229
|
-
|
|
272
|
+
# Handle dictionary case where instruments is {exchange: [symbols]}
|
|
273
|
+
if isinstance(instruments, dict):
|
|
274
|
+
for exchange_name, symbol_list in instruments.items():
|
|
275
|
+
if exchange_name not in _exchanges:
|
|
276
|
+
_exchanges.append(exchange_name)
|
|
230
277
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
if (ix := lookup.find_symbol(_e, _s)) is not None:
|
|
237
|
-
_exchanges.append(_e.upper())
|
|
238
|
-
_instrs.append(ix)
|
|
239
|
-
else:
|
|
240
|
-
logger.warning(f"Can't find instrument for specified symbol ({i}) - ignoring !")
|
|
278
|
+
for symbol in symbol_list:
|
|
279
|
+
instrument, resolved_exchange = _process_single_symbol_or_instrument(symbol, exchange_name, exchange)
|
|
280
|
+
if instrument is not None and resolved_exchange is not None:
|
|
281
|
+
_instrs.append(instrument)
|
|
282
|
+
_exchanges.append(resolved_exchange)
|
|
241
283
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
284
|
+
# Handle list case
|
|
285
|
+
else:
|
|
286
|
+
for symbol in instruments:
|
|
287
|
+
instrument, resolved_exchange = _process_single_symbol_or_instrument(symbol, exchange, exchange)
|
|
288
|
+
if instrument is not None and resolved_exchange is not None:
|
|
289
|
+
_instrs.append(instrument)
|
|
290
|
+
_exchanges.append(resolved_exchange)
|
|
248
291
|
|
|
249
292
|
return _instrs, list(set(_exchanges))
|
|
250
293
|
|
|
@@ -7,6 +7,7 @@ from qubx.core.basics import (
|
|
|
7
7
|
ZERO_COSTS,
|
|
8
8
|
AssetBalance,
|
|
9
9
|
Deal,
|
|
10
|
+
FundingPayment,
|
|
10
11
|
Instrument,
|
|
11
12
|
ITimeProvider,
|
|
12
13
|
Order,
|
|
@@ -104,18 +105,23 @@ class BasicAccountProcessor(IAccountProcessor):
|
|
|
104
105
|
########################################################
|
|
105
106
|
def get_leverage(self, instrument: Instrument) -> float:
|
|
106
107
|
pos = self._positions.get(instrument)
|
|
108
|
+
capital = self.get_total_capital()
|
|
109
|
+
if np.isclose(capital, 0):
|
|
110
|
+
return 0.0
|
|
107
111
|
if pos is not None:
|
|
108
|
-
return pos.notional_value /
|
|
112
|
+
return pos.notional_value / capital
|
|
109
113
|
return 0.0
|
|
110
114
|
|
|
111
115
|
def get_leverages(self, exchange: str | None = None) -> dict[Instrument, float]:
|
|
112
116
|
return {s: self.get_leverage(s) for s in self._positions.keys()}
|
|
113
117
|
|
|
114
118
|
def get_net_leverage(self, exchange: str | None = None) -> float:
|
|
115
|
-
|
|
119
|
+
leverages = self.get_leverages(exchange).values()
|
|
120
|
+
return sum(lev for lev in leverages if lev is not None and not np.isnan(lev))
|
|
116
121
|
|
|
117
122
|
def get_gross_leverage(self, exchange: str | None = None) -> float:
|
|
118
|
-
|
|
123
|
+
leverages = self.get_leverages(exchange).values()
|
|
124
|
+
return sum(abs(lev) for lev in leverages if lev is not None and not np.isnan(lev))
|
|
119
125
|
|
|
120
126
|
########################################################
|
|
121
127
|
# Margin information
|
|
@@ -232,6 +238,36 @@ class BasicAccountProcessor(IAccountProcessor):
|
|
|
232
238
|
self._balances[self.base_currency] -= fee_in_base
|
|
233
239
|
self._balances[instrument.settle] += realized_pnl
|
|
234
240
|
|
|
241
|
+
def process_funding_payment(self, instrument: Instrument, funding_payment: FundingPayment) -> None:
|
|
242
|
+
"""Process funding payment for an instrument.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
instrument: Instrument the funding payment applies to
|
|
246
|
+
funding_payment: Funding payment event to process
|
|
247
|
+
"""
|
|
248
|
+
pos = self._positions.get(instrument)
|
|
249
|
+
|
|
250
|
+
if pos is None or not instrument.is_futures():
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
# Get current market price for funding calculation
|
|
254
|
+
# We need to get the mark price from the market data, but since we don't have access
|
|
255
|
+
# to market data here, we'll use the current position price as a reasonable fallback
|
|
256
|
+
mark_price = pos.position_avg_price_funds if pos.position_avg_price_funds > 0 else 0.0
|
|
257
|
+
|
|
258
|
+
# Apply funding payment to position
|
|
259
|
+
funding_amount = pos.apply_funding_payment(funding_payment, mark_price)
|
|
260
|
+
|
|
261
|
+
# Update account balance with funding payment
|
|
262
|
+
# For futures contracts, funding affects the settlement currency balance
|
|
263
|
+
self._balances[instrument.settle] += funding_amount
|
|
264
|
+
|
|
265
|
+
logger.debug(
|
|
266
|
+
f" [<y>{self.__class__.__name__}</y>(<g>{instrument}</g>)] :: "
|
|
267
|
+
f"funding payment {funding_amount:.6f} {instrument.settle} "
|
|
268
|
+
f"(rate: {funding_payment.funding_rate:.6f})"
|
|
269
|
+
)
|
|
270
|
+
|
|
235
271
|
def _fill_missing_fee_info(self, instrument: Instrument, deals: list[Deal]) -> None:
|
|
236
272
|
for d in deals:
|
|
237
273
|
if d.fee_amount is None:
|
|
@@ -352,16 +388,48 @@ class CompositeAccountProcessor(IAccountProcessor):
|
|
|
352
388
|
return self._account_processors[exch].get_capital()
|
|
353
389
|
|
|
354
390
|
def get_total_capital(self, exchange: str | None = None) -> float:
|
|
355
|
-
|
|
356
|
-
|
|
391
|
+
if exchange is not None:
|
|
392
|
+
# Return total capital from specific exchange
|
|
393
|
+
exch = self._get_exchange(exchange)
|
|
394
|
+
return self._account_processors[exch].get_total_capital()
|
|
395
|
+
|
|
396
|
+
# Return aggregated total capital from all exchanges when no exchange is specified
|
|
397
|
+
total_capital = 0.0
|
|
398
|
+
for exch_name, processor in self._account_processors.items():
|
|
399
|
+
total_capital += processor.get_total_capital()
|
|
400
|
+
return total_capital
|
|
357
401
|
|
|
358
402
|
def get_balances(self, exchange: str | None = None) -> dict[str, AssetBalance]:
|
|
359
|
-
|
|
360
|
-
|
|
403
|
+
if exchange is not None:
|
|
404
|
+
# Return balances from specific exchange
|
|
405
|
+
exch = self._get_exchange(exchange)
|
|
406
|
+
return self._account_processors[exch].get_balances()
|
|
407
|
+
|
|
408
|
+
# Return aggregated balances from all exchanges when no exchange is specified
|
|
409
|
+
all_balances: dict[str, AssetBalance] = defaultdict(lambda: AssetBalance())
|
|
410
|
+
for exch_name, processor in self._account_processors.items():
|
|
411
|
+
exch_balances = processor.get_balances()
|
|
412
|
+
for currency, balance in exch_balances.items():
|
|
413
|
+
if currency not in all_balances:
|
|
414
|
+
all_balances[currency] = AssetBalance(balance.free, balance.locked, balance.total)
|
|
415
|
+
else:
|
|
416
|
+
all_balances[currency].free += balance.free
|
|
417
|
+
all_balances[currency].locked += balance.locked
|
|
418
|
+
all_balances[currency].total += balance.total
|
|
419
|
+
return dict(all_balances)
|
|
361
420
|
|
|
362
421
|
def get_positions(self, exchange: str | None = None) -> dict[Instrument, Position]:
|
|
363
|
-
|
|
364
|
-
|
|
422
|
+
if exchange is not None:
|
|
423
|
+
# Return positions from specific exchange
|
|
424
|
+
exch = self._get_exchange(exchange)
|
|
425
|
+
return self._account_processors[exch].get_positions()
|
|
426
|
+
|
|
427
|
+
# Return positions from all exchanges when no exchange is specified
|
|
428
|
+
all_positions: dict[Instrument, Position] = {}
|
|
429
|
+
for exch_name, processor in self._account_processors.items():
|
|
430
|
+
exch_positions = processor.get_positions()
|
|
431
|
+
all_positions.update(exch_positions)
|
|
432
|
+
return all_positions
|
|
365
433
|
|
|
366
434
|
def get_position(self, instrument: Instrument) -> Position:
|
|
367
435
|
exch = self._get_exchange(instrument=instrument)
|
|
@@ -453,3 +521,7 @@ class CompositeAccountProcessor(IAccountProcessor):
|
|
|
453
521
|
def process_deals(self, instrument: Instrument, deals: list[Deal]) -> None:
|
|
454
522
|
exch = self._get_exchange(instrument=instrument)
|
|
455
523
|
self._account_processors[exch].process_deals(instrument, deals)
|
|
524
|
+
|
|
525
|
+
def process_funding_payment(self, instrument: Instrument, funding_payment: FundingPayment) -> None:
|
|
526
|
+
exch = self._get_exchange(instrument=instrument)
|
|
527
|
+
self._account_processors[exch].process_funding_payment(instrument, funding_payment)
|