Qubx 0.6.64__tar.gz → 0.6.66__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.64 → qubx-0.6.66}/PKG-INFO +1 -1
- {qubx-0.6.64 → qubx-0.6.66}/pyproject.toml +1 -1
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/backtester/broker.py +9 -2
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/backtester/data.py +1 -1
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/backtester/runner.py +116 -12
- qubx-0.6.66/src/qubx/backtester/sentinels.py +23 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/backtester/simulated_data.py +23 -9
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/backtester/simulated_exchange.py +6 -3
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/backtester/simulator.py +39 -6
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/backtester/utils.py +48 -15
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/connectors/ccxt/data.py +1 -1
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/connectors/ccxt/reader.py +4 -4
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/connectors/ccxt/utils.py +3 -3
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/basics.py +18 -23
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/context.py +24 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/helpers.py +21 -4
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/initializer.py +86 -1
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/interfaces.py +82 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/metrics.py +110 -5
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/mixins/processing.py +96 -7
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/mixins/trading.py +34 -4
- qubx-0.6.66/src/qubx/core/stale_data_detector.py +418 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/data/__init__.py +2 -1
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/data/composite.py +7 -4
- qubx-0.6.66/src/qubx/data/helpers.py +2054 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/data/readers.py +21 -8
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/emitters/base.py +1 -1
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/gathering/simplest.py +3 -1
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/restarts/state_resolvers.py +5 -1
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/trackers/__init__.py +2 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/trackers/riskctrl.py +13 -2
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/trackers/sizers.py +56 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/runner/_jupyter_runner.pyt +9 -2
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/runner/configs.py +11 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/runner/runner.py +7 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/time.py +56 -0
- qubx-0.6.64/src/qubx/data/helpers.py +0 -443
- {qubx-0.6.64 → qubx-0.6.66}/LICENSE +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/README.md +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/build.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/backtester/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/backtester/account.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/backtester/management.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/backtester/ome.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/backtester/optimization.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/cli/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/cli/commands.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/cli/deploy.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/cli/misc.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/cli/release.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/cli/tui.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/account.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/deque.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/errors.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/exceptions.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/loggers.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/lookups.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/mixins/market.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/series.pxd +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/series.pyi +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/series.pyx +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/data/hft.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/data/registry.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/data/tardis.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/emitters/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/emitters/composite.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/emitters/csv.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/emitters/indicator.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/emitters/questdb.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/exporters/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/exporters/composite.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/exporters/slack.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/features/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/features/core.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/features/orderbook.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/features/price.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/features/trades.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/features/utils.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/health/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/health/base.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/loggers/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/loggers/csv.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/loggers/factory.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/loggers/mongo.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/math/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/math/stats.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/notifications/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/notifications/composite.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/notifications/slack.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/notifications/throttler.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/pandaz/ta.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/pandaz/utils.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/resources/_build.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-hyperliquid-spot.json +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-hyperliquid.f-perpetual.json +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/restarts/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/restorers/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/restorers/balance.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/restorers/factory.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/restorers/position.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/restorers/signal.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/restorers/state.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/restorers/utils.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/trackers/advanced.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/collections.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/misc.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/orderbook.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/questdb.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-0.6.64 → qubx-0.6.66}/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.66"
|
|
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"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from qubx import logger
|
|
1
2
|
from qubx.backtester.ome import SimulatedExecutionReport
|
|
2
3
|
from qubx.backtester.simulated_exchange import ISimulatedExchange
|
|
3
4
|
from qubx.core.basics import (
|
|
@@ -5,6 +6,7 @@ from qubx.core.basics import (
|
|
|
5
6
|
Instrument,
|
|
6
7
|
Order,
|
|
7
8
|
)
|
|
9
|
+
from qubx.core.exceptions import OrderNotFound
|
|
8
10
|
from qubx.core.interfaces import IBroker
|
|
9
11
|
|
|
10
12
|
from .account import SimulatedAccountProcessor
|
|
@@ -63,8 +65,13 @@ class SimulatedBroker(IBroker):
|
|
|
63
65
|
self.send_order(instrument, order_side, order_type, amount, price, client_id, time_in_force, **optional)
|
|
64
66
|
|
|
65
67
|
def cancel_order(self, order_id: str) -> Order | None:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
try:
|
|
69
|
+
self._send_execution_report(order_update := self._exchange.cancel_order(order_id))
|
|
70
|
+
return order_update.order if order_update is not None else None
|
|
71
|
+
except OrderNotFound:
|
|
72
|
+
# Order was already cancelled or doesn't exist
|
|
73
|
+
logger.debug(f"Order {order_id} not found")
|
|
74
|
+
return None
|
|
68
75
|
|
|
69
76
|
def cancel_orders(self, instrument: Instrument) -> None:
|
|
70
77
|
raise NotImplementedError("Not implemented yet")
|
|
@@ -71,7 +71,7 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
71
71
|
# Check if the instrument was actually subscribed (not filtered out)
|
|
72
72
|
if not self.has_subscription(i, subscription_type):
|
|
73
73
|
continue
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
h_data = self._data_source.peek_historical_data(i, subscription_type)
|
|
76
76
|
if h_data:
|
|
77
77
|
# _s_type = DataType.from_str(subscription_type)[0]
|
|
@@ -5,7 +5,9 @@ import pandas as pd
|
|
|
5
5
|
from tqdm.auto import tqdm
|
|
6
6
|
|
|
7
7
|
from qubx import logger
|
|
8
|
+
from qubx.backtester.sentinels import NoDataContinue
|
|
8
9
|
from qubx.backtester.simulated_data import IterableSimulationData
|
|
10
|
+
from qubx.backtester.utils import SimulationDataConfig, TimeGuardedWrapper
|
|
9
11
|
from qubx.core.account import CompositeAccountProcessor
|
|
10
12
|
from qubx.core.basics import SW, DataType, Instrument, TransactionCostsCalculator
|
|
11
13
|
from qubx.core.context import StrategyContext
|
|
@@ -22,8 +24,10 @@ from qubx.core.interfaces import (
|
|
|
22
24
|
)
|
|
23
25
|
from qubx.core.loggers import StrategyLogging
|
|
24
26
|
from qubx.core.lookups import lookup
|
|
27
|
+
from qubx.data.helpers import CachedPrefetchReader
|
|
25
28
|
from qubx.loggers.inmemory import InMemoryLogsWriter
|
|
26
29
|
from qubx.pandaz.utils import _frame_to_str
|
|
30
|
+
from qubx.utils.time import now_ns
|
|
27
31
|
|
|
28
32
|
from .account import SimulatedAccountProcessor
|
|
29
33
|
from .broker import SimulatedBroker
|
|
@@ -80,6 +84,7 @@ class SimulationRunner:
|
|
|
80
84
|
emitter: IMetricEmitter | None = None,
|
|
81
85
|
strategy_state: StrategyState | None = None,
|
|
82
86
|
initializer: BasicStrategyInitializer | None = None,
|
|
87
|
+
warmup_mode: bool = False,
|
|
83
88
|
):
|
|
84
89
|
"""
|
|
85
90
|
Initialize the BacktestContextRunner with a strategy context.
|
|
@@ -102,8 +107,10 @@ class SimulationRunner:
|
|
|
102
107
|
self.emitter = emitter
|
|
103
108
|
self.strategy_state = strategy_state if strategy_state is not None else StrategyState()
|
|
104
109
|
self.initializer = initializer
|
|
110
|
+
self.warmup_mode = warmup_mode
|
|
105
111
|
self._pregenerated_signals = dict()
|
|
106
112
|
self._to_process = {}
|
|
113
|
+
self._aux_data_reader = None
|
|
107
114
|
|
|
108
115
|
# - get strategy parameters BEFORE simulation start
|
|
109
116
|
# potentially strategy may change it's parameters during simulation
|
|
@@ -126,6 +133,8 @@ class SimulationRunner:
|
|
|
126
133
|
"""
|
|
127
134
|
logger.debug(f"[<y>SimulationRunner</y>] :: Running simulation from {self.start} to {self.stop}")
|
|
128
135
|
|
|
136
|
+
self._prefetch_aux_data()
|
|
137
|
+
|
|
129
138
|
# Start the context
|
|
130
139
|
self.ctx.start()
|
|
131
140
|
|
|
@@ -164,6 +173,8 @@ class SimulationRunner:
|
|
|
164
173
|
logger.error("Simulated trading interrupted by user!")
|
|
165
174
|
if not catch_keyboard_interrupt:
|
|
166
175
|
raise
|
|
176
|
+
except Exception as e:
|
|
177
|
+
raise e
|
|
167
178
|
finally:
|
|
168
179
|
# Stop the context
|
|
169
180
|
self.ctx.stop()
|
|
@@ -196,7 +207,7 @@ class SimulationRunner:
|
|
|
196
207
|
for i in self._data_providers[0].get_subscribed_instruments():
|
|
197
208
|
# - we can process series with variable id's if we can find some similar instrument
|
|
198
209
|
if s == i.symbol or s == str(i) or s == f"{i.exchange}:{i.symbol}" or str(s) == str(i):
|
|
199
|
-
_start, _end =
|
|
210
|
+
_start, _end = np.datetime64(start), np.datetime64(end)
|
|
200
211
|
_start_idx, _end_idx = v.index.get_indexer([_start, _end], method="ffill")
|
|
201
212
|
sel = v.iloc[max(_start_idx, 0) : _end_idx + 1]
|
|
202
213
|
|
|
@@ -268,21 +279,34 @@ class SimulationRunner:
|
|
|
268
279
|
start, stop = pd.Timestamp(start), pd.Timestamp(stop)
|
|
269
280
|
total_duration = stop - start
|
|
270
281
|
update_delta = total_duration / 100
|
|
271
|
-
prev_dt =
|
|
282
|
+
prev_dt = np.datetime64(start)
|
|
272
283
|
|
|
273
284
|
# - date iteration
|
|
274
285
|
qiter = self._data_source.create_iterable(start, stop)
|
|
286
|
+
|
|
275
287
|
if silent:
|
|
276
288
|
for instrument, data_type, event, is_hist in qiter:
|
|
277
|
-
|
|
289
|
+
# Handle NoDataContinue sentinel
|
|
290
|
+
if isinstance(event, NoDataContinue):
|
|
291
|
+
if not self._handle_no_data_scenario(stop):
|
|
292
|
+
break
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
if not self._process_event(instrument, data_type, event, is_hist, _run, stop):
|
|
278
296
|
break
|
|
279
297
|
else:
|
|
280
298
|
_p = 0
|
|
281
299
|
with tqdm(total=100, desc="Simulating", unit="%", leave=False) as pbar:
|
|
282
300
|
for instrument, data_type, event, is_hist in qiter:
|
|
283
|
-
|
|
301
|
+
# Handle NoDataContinue sentinel
|
|
302
|
+
if isinstance(event, NoDataContinue):
|
|
303
|
+
if not self._handle_no_data_scenario(stop):
|
|
304
|
+
break
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
if not self._process_event(instrument, data_type, event, is_hist, _run, stop):
|
|
284
308
|
break
|
|
285
|
-
dt =
|
|
309
|
+
dt = np.datetime64(event.time, "ns")
|
|
286
310
|
# update only if date has changed
|
|
287
311
|
if dt - prev_dt > update_delta:
|
|
288
312
|
_p += 1
|
|
@@ -294,6 +318,43 @@ class SimulationRunner:
|
|
|
294
318
|
|
|
295
319
|
logger.info(f"{self.__class__.__name__} ::: Simulation finished at {stop} :::")
|
|
296
320
|
|
|
321
|
+
def _process_event(self, instrument, data_type, event, is_hist, _run, stop_time):
|
|
322
|
+
"""Process a single simulation event with proper time advancement and scheduler checks."""
|
|
323
|
+
# During warmup, clamp future timestamps to current time
|
|
324
|
+
if self.warmup_mode and hasattr(event, "time"):
|
|
325
|
+
current_real_time = now_ns()
|
|
326
|
+
if event.time > current_real_time:
|
|
327
|
+
event.time = current_real_time
|
|
328
|
+
|
|
329
|
+
if not _run(instrument, data_type, event, is_hist):
|
|
330
|
+
return False
|
|
331
|
+
return True
|
|
332
|
+
|
|
333
|
+
def _handle_no_data_scenario(self, stop_time):
|
|
334
|
+
"""Handle scenario when no data is available but scheduler might have events."""
|
|
335
|
+
# Check if we have pending scheduled events
|
|
336
|
+
if hasattr(self.scheduler, "_next_nearest_time"):
|
|
337
|
+
next_scheduled_time = self.scheduler._next_nearest_time
|
|
338
|
+
current_time = self.time_provider.time()
|
|
339
|
+
|
|
340
|
+
# Convert to int64 for numerical comparisons (avoid type issues)
|
|
341
|
+
next_time_ns = next_scheduled_time.astype("int64")
|
|
342
|
+
current_time_ns = current_time.astype("int64")
|
|
343
|
+
stop_time_ns = stop_time.value # Already int64
|
|
344
|
+
|
|
345
|
+
# Check if we've reached the stop time
|
|
346
|
+
if current_time_ns >= stop_time_ns:
|
|
347
|
+
return False # Stop simulation
|
|
348
|
+
|
|
349
|
+
# If there's a scheduled event before stop time, advance to it
|
|
350
|
+
if next_time_ns < np.iinfo(np.int64).max and next_time_ns < stop_time_ns:
|
|
351
|
+
# Use the original datetime64 object for set_time (not the int64 conversion)
|
|
352
|
+
self.time_provider.set_time(next_scheduled_time)
|
|
353
|
+
self.scheduler.check_and_run_tasks()
|
|
354
|
+
return True # Continue simulation
|
|
355
|
+
|
|
356
|
+
return False # No scheduled events, stop simulation
|
|
357
|
+
|
|
297
358
|
def print_latency_report(self) -> None:
|
|
298
359
|
_l_r = SW.latency_report()
|
|
299
360
|
if _l_r is not None:
|
|
@@ -311,11 +372,6 @@ class SimulationRunner:
|
|
|
311
372
|
f"for {self.setup.capital} {self.setup.base_currency}..."
|
|
312
373
|
)
|
|
313
374
|
|
|
314
|
-
data_source = IterableSimulationData(
|
|
315
|
-
self.data_config.data_providers,
|
|
316
|
-
open_close_time_indent_secs=self.data_config.adjusted_open_close_time_indent_secs,
|
|
317
|
-
)
|
|
318
|
-
|
|
319
375
|
channel = SimulatedCtrlChannel("databus", sentinel=(None, None, None, None))
|
|
320
376
|
simulated_clock = SimulatedTimeProvider(np.datetime64(self.start, "ns"))
|
|
321
377
|
|
|
@@ -325,6 +381,11 @@ class SimulationRunner:
|
|
|
325
381
|
|
|
326
382
|
scheduler = SimulatedScheduler(channel, lambda: simulated_clock.time().item())
|
|
327
383
|
|
|
384
|
+
data_source = IterableSimulationData(
|
|
385
|
+
self.data_config.data_providers,
|
|
386
|
+
open_close_time_indent_secs=self.data_config.adjusted_open_close_time_indent_secs,
|
|
387
|
+
)
|
|
388
|
+
|
|
328
389
|
brokers = []
|
|
329
390
|
for exchange in self.setup.exchanges:
|
|
330
391
|
_exchange_account = account.get_account_processor(exchange)
|
|
@@ -349,7 +410,7 @@ class SimulationRunner:
|
|
|
349
410
|
)
|
|
350
411
|
|
|
351
412
|
# - get aux data provider
|
|
352
|
-
|
|
413
|
+
self._aux_data_reader = self.data_config.get_timeguarded_aux_reader(simulated_clock)
|
|
353
414
|
|
|
354
415
|
# - it will store simulation results into memory
|
|
355
416
|
logs_writer = InMemoryLogsWriter(self.account_id, self.setup.name, "0")
|
|
@@ -401,7 +462,7 @@ class SimulationRunner:
|
|
|
401
462
|
time_provider=simulated_clock,
|
|
402
463
|
instruments=self.setup.instruments,
|
|
403
464
|
logging=StrategyLogging(logs_writer, portfolio_log_freq=self.portfolio_log_freq),
|
|
404
|
-
aux_data_provider=
|
|
465
|
+
aux_data_provider=self._aux_data_reader,
|
|
405
466
|
emitter=self.emitter,
|
|
406
467
|
strategy_state=self.strategy_state,
|
|
407
468
|
initializer=self.initializer,
|
|
@@ -422,6 +483,10 @@ class SimulationRunner:
|
|
|
422
483
|
logger.debug(f"[<y>simulator</y>] :: Setting default schedule: {self.data_config.default_trigger_schedule}")
|
|
423
484
|
ctx.set_event_schedule(self.data_config.default_trigger_schedule)
|
|
424
485
|
|
|
486
|
+
if self.setup.enable_funding:
|
|
487
|
+
logger.debug("[<y>simulator</y>] :: Enabling funding rate simulation")
|
|
488
|
+
ctx.subscribe(DataType.FUNDING_PAYMENT)
|
|
489
|
+
|
|
425
490
|
self.logs_writer = logs_writer
|
|
426
491
|
self.channel = channel
|
|
427
492
|
self.time_provider = simulated_clock
|
|
@@ -483,3 +548,42 @@ class SimulationRunner:
|
|
|
483
548
|
time_provider=time_provider,
|
|
484
549
|
account_processors=_account_processors,
|
|
485
550
|
)
|
|
551
|
+
|
|
552
|
+
def _prefetch_aux_data(self):
|
|
553
|
+
# Perform prefetch of aux data if enabled
|
|
554
|
+
if self._aux_data_reader is None:
|
|
555
|
+
return
|
|
556
|
+
|
|
557
|
+
aux_reader = self._aux_data_reader
|
|
558
|
+
if isinstance(aux_reader, TimeGuardedWrapper) and isinstance(aux_reader._reader, CachedPrefetchReader):
|
|
559
|
+
aux_reader = aux_reader._reader
|
|
560
|
+
elif isinstance(aux_reader, CachedPrefetchReader):
|
|
561
|
+
aux_reader = aux_reader
|
|
562
|
+
else:
|
|
563
|
+
return
|
|
564
|
+
|
|
565
|
+
if self.data_config.prefetch_config and self.data_config.prefetch_config.enabled:
|
|
566
|
+
# Prepare prefetch arguments
|
|
567
|
+
prefetch_args = self.data_config.prefetch_config.args.copy()
|
|
568
|
+
|
|
569
|
+
# Add exchange info if available from instruments
|
|
570
|
+
if self.setup.instruments and "exchange" not in prefetch_args:
|
|
571
|
+
# Get exchange from first instrument
|
|
572
|
+
first_exchange = self.setup.instruments[0].exchange
|
|
573
|
+
if first_exchange:
|
|
574
|
+
prefetch_args["exchange"] = first_exchange
|
|
575
|
+
|
|
576
|
+
logger.info(
|
|
577
|
+
f"Prefetching aux data: {self.data_config.prefetch_config.aux_data_names} for period {self.start} to {self.stop}"
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
try:
|
|
581
|
+
# Perform the prefetch
|
|
582
|
+
aux_reader.prefetch_aux_data(
|
|
583
|
+
self.data_config.prefetch_config.aux_data_names,
|
|
584
|
+
start=str(self.start),
|
|
585
|
+
stop=str(self.stop),
|
|
586
|
+
**prefetch_args,
|
|
587
|
+
)
|
|
588
|
+
except Exception as e:
|
|
589
|
+
logger.warning(f"Prefetch failed: {e}")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NoDataContinue:
|
|
7
|
+
"""Sentinel indicating no data streams available but simulation should continue.
|
|
8
|
+
|
|
9
|
+
This is used when all instruments are unsubscribed but there may still be
|
|
10
|
+
scheduled events to process before the simulation stop time.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, next_scheduled_time: Optional[int] = None):
|
|
14
|
+
"""Initialize the sentinel.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
next_scheduled_time: The next scheduled event time in nanoseconds,
|
|
18
|
+
or None if no scheduled events exist.
|
|
19
|
+
"""
|
|
20
|
+
self.next_scheduled_time = next_scheduled_time
|
|
21
|
+
|
|
22
|
+
def __repr__(self) -> str:
|
|
23
|
+
return f"NoDataContinue(next_scheduled_time={self.next_scheduled_time})"
|
|
@@ -3,6 +3,7 @@ from typing import Any, Iterator
|
|
|
3
3
|
import pandas as pd
|
|
4
4
|
|
|
5
5
|
from qubx import logger
|
|
6
|
+
from qubx.backtester.sentinels import NoDataContinue
|
|
6
7
|
from qubx.core.basics import DataType, Instrument, MarketType, Timestamped
|
|
7
8
|
from qubx.core.exceptions import SimulationError
|
|
8
9
|
from qubx.data.composite import IteratedDataStreamsSlicer
|
|
@@ -259,14 +260,14 @@ class IterableSimulationData(Iterator):
|
|
|
259
260
|
def _filter_instruments_for_subscription(self, data_type: str, instruments: list[Instrument]) -> list[Instrument]:
|
|
260
261
|
"""
|
|
261
262
|
Filter instruments based on subscription type requirements.
|
|
262
|
-
|
|
263
|
+
|
|
263
264
|
For funding payment subscriptions, only SWAP instruments are supported since
|
|
264
265
|
funding payments are specific to perpetual swap contracts.
|
|
265
|
-
|
|
266
|
+
|
|
266
267
|
Args:
|
|
267
268
|
data_type: The data type being subscribed to
|
|
268
269
|
instruments: List of instruments to filter
|
|
269
|
-
|
|
270
|
+
|
|
270
271
|
Returns:
|
|
271
272
|
Filtered list of instruments appropriate for the subscription type
|
|
272
273
|
"""
|
|
@@ -275,23 +276,25 @@ class IterableSimulationData(Iterator):
|
|
|
275
276
|
original_count = len(instruments)
|
|
276
277
|
filtered_instruments = [i for i in instruments if i.market_type == MarketType.SWAP]
|
|
277
278
|
filtered_count = len(filtered_instruments)
|
|
278
|
-
|
|
279
|
+
|
|
279
280
|
# Log if instruments were filtered out (debug info)
|
|
280
281
|
if filtered_count < original_count:
|
|
281
|
-
logger.debug(
|
|
282
|
-
|
|
282
|
+
logger.debug(
|
|
283
|
+
f"Filtered {original_count - filtered_count} non-SWAP instruments from funding payment subscription"
|
|
284
|
+
)
|
|
285
|
+
|
|
283
286
|
return filtered_instruments
|
|
284
|
-
|
|
287
|
+
|
|
285
288
|
# For all other subscription types, return instruments unchanged
|
|
286
289
|
return instruments
|
|
287
290
|
|
|
288
291
|
def add_instruments_for_subscription(self, subscription: str, instruments: list[Instrument] | Instrument):
|
|
289
292
|
instruments = instruments if isinstance(instruments, list) else [instruments]
|
|
290
293
|
_subt_key, _data_type, _params = self._parse_subscription_spec(subscription)
|
|
291
|
-
|
|
294
|
+
|
|
292
295
|
# Filter instruments based on subscription type requirements
|
|
293
296
|
instruments = self._filter_instruments_for_subscription(_data_type, instruments)
|
|
294
|
-
|
|
297
|
+
|
|
295
298
|
# If no instruments remain after filtering, skip subscription
|
|
296
299
|
if not instruments:
|
|
297
300
|
return
|
|
@@ -445,6 +448,17 @@ class IterableSimulationData(Iterator):
|
|
|
445
448
|
try:
|
|
446
449
|
while data := next(self._slicing_iterator): # type: ignore
|
|
447
450
|
k, t, v = data
|
|
451
|
+
|
|
452
|
+
# Check if we've reached or exceeded the stop time
|
|
453
|
+
# It's commented out because we expect data readers to stop on their own
|
|
454
|
+
# if self._stop is not None and t > self._stop.value:
|
|
455
|
+
# raise StopIteration
|
|
456
|
+
|
|
457
|
+
# Handle NoDataContinue sentinel
|
|
458
|
+
if isinstance(v, NoDataContinue):
|
|
459
|
+
# Return the sentinel as the event - the runner will detect it with isinstance
|
|
460
|
+
return None, "", v, False
|
|
461
|
+
|
|
448
462
|
instr, fetcher, subt = self._instruments[k]
|
|
449
463
|
data_type = fetcher._producing_data_type
|
|
450
464
|
_is_historical = False
|
|
@@ -11,6 +11,7 @@ from qubx.core.basics import (
|
|
|
11
11
|
TransactionCostsCalculator,
|
|
12
12
|
dt_64,
|
|
13
13
|
)
|
|
14
|
+
from qubx.core.exceptions import OrderNotFound
|
|
14
15
|
from qubx.core.series import Bar, OrderBook, Quote, Trade, TradeArray
|
|
15
16
|
|
|
16
17
|
|
|
@@ -155,8 +156,7 @@ class BasicSimulatedExchange(ISimulatedExchange):
|
|
|
155
156
|
if order.id == order_id:
|
|
156
157
|
return self._process_ome_response(o.cancel_order(order_id))
|
|
157
158
|
|
|
158
|
-
|
|
159
|
-
return None
|
|
159
|
+
raise OrderNotFound(f"Order '{order_id}' not found")
|
|
160
160
|
|
|
161
161
|
ome = self._ome.get(instrument)
|
|
162
162
|
if ome is None:
|
|
@@ -165,7 +165,10 @@ class BasicSimulatedExchange(ISimulatedExchange):
|
|
|
165
165
|
)
|
|
166
166
|
|
|
167
167
|
# - cancel order in OME and remove from the map to free memory
|
|
168
|
-
|
|
168
|
+
result = self._process_ome_response(ome.cancel_order(order_id))
|
|
169
|
+
if result is None:
|
|
170
|
+
raise OrderNotFound(f"Order '{order_id}' not found")
|
|
171
|
+
return result
|
|
169
172
|
|
|
170
173
|
def _process_ome_response(self, report: SimulatedExecutionReport | None) -> SimulatedExecutionReport | None:
|
|
171
174
|
if report is not None:
|
|
@@ -4,13 +4,15 @@ import pandas as pd
|
|
|
4
4
|
from joblib import delayed
|
|
5
5
|
|
|
6
6
|
from qubx import QubxLogConfig, logger
|
|
7
|
+
from qubx.backtester.utils import SetupTypes
|
|
7
8
|
from qubx.core.basics import Instrument
|
|
8
9
|
from qubx.core.exceptions import SimulationError
|
|
9
10
|
from qubx.core.metrics import TradingSessionResult
|
|
10
11
|
from qubx.data.readers import DataReader
|
|
11
12
|
from qubx.emitters.inmemory import InMemoryMetricEmitter
|
|
12
13
|
from qubx.utils.misc import ProgressParallel, Stopwatch, get_current_user
|
|
13
|
-
from qubx.utils.
|
|
14
|
+
from qubx.utils.runner.configs import PrefetchConfig
|
|
15
|
+
from qubx.utils.time import handle_start_stop, to_utc_naive
|
|
14
16
|
|
|
15
17
|
from .runner import SimulationRunner
|
|
16
18
|
from .utils import (
|
|
@@ -31,10 +33,10 @@ def simulate(
|
|
|
31
33
|
strategies: StrategiesDecls_t,
|
|
32
34
|
data: DataDecls_t,
|
|
33
35
|
capital: float | dict[str, float],
|
|
34
|
-
instruments: list[str] | list[Instrument] | dict[ExchangeName_t, list[SymbolOrInstrument_t]],
|
|
35
|
-
commissions: str | dict[str, str | None] | None,
|
|
36
36
|
start: str | pd.Timestamp,
|
|
37
37
|
stop: str | pd.Timestamp | None = None,
|
|
38
|
+
instruments: list[str] | list[Instrument] | dict[ExchangeName_t, list[SymbolOrInstrument_t]] | None = None,
|
|
39
|
+
commissions: str | dict[str, str | None] | None = None,
|
|
38
40
|
exchange: ExchangeName_t | list[ExchangeName_t] | None = None,
|
|
39
41
|
base_currency: str = "USDT",
|
|
40
42
|
n_jobs: int = 1,
|
|
@@ -43,6 +45,7 @@ def simulate(
|
|
|
43
45
|
accurate_stop_orders_execution: bool = False,
|
|
44
46
|
signal_timeframe: str = "1Min",
|
|
45
47
|
open_close_time_indent_secs=1,
|
|
48
|
+
enable_funding: bool = False,
|
|
46
49
|
debug: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = "WARNING",
|
|
47
50
|
show_latency_report: bool = False,
|
|
48
51
|
portfolio_log_freq: str = "5Min",
|
|
@@ -50,6 +53,7 @@ def simulate(
|
|
|
50
53
|
enable_inmemory_emitter: bool = False,
|
|
51
54
|
emitter_stats_interval: str = "1h",
|
|
52
55
|
run_separate_instruments: bool = False,
|
|
56
|
+
prefetch_config: PrefetchConfig | None = None,
|
|
53
57
|
) -> list[TradingSessionResult]:
|
|
54
58
|
"""
|
|
55
59
|
Backtest utility for trading strategies or signals using historical data.
|
|
@@ -70,6 +74,7 @@ def simulate(
|
|
|
70
74
|
- accurate_stop_orders_execution (bool): If True, enables more accurate stop order execution simulation.
|
|
71
75
|
- signal_timeframe (str): Timeframe for signals, default is "1Min".
|
|
72
76
|
- open_close_time_indent_secs (int): Time indent in seconds for open/close times, default is 1.
|
|
77
|
+
- enable_funding (bool): If True, enables funding rate simulation, default is False.
|
|
73
78
|
- debug (Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None): Logging level for debugging.
|
|
74
79
|
- show_latency_report: If True, shows simulator's latency report.
|
|
75
80
|
- portfolio_log_freq (str): Frequency for portfolio logging, default is "5Min".
|
|
@@ -77,6 +82,7 @@ def simulate(
|
|
|
77
82
|
- enable_inmemory_emitter (bool): If True, attaches an in-memory metric emitter and returns its dataframe in TradingSessionResult.emitter_data.
|
|
78
83
|
- emitter_stats_interval (str): Interval for emitting stats in the in-memory emitter (default: "1h").
|
|
79
84
|
- run_separate_instruments (bool): If True, creates separate simulation setups for each instrument, default is False.
|
|
85
|
+
- prefetch_config (dict[str, Any] | None): Configuration for prefetching auxiliary data, default is None.
|
|
80
86
|
|
|
81
87
|
Returns:
|
|
82
88
|
- list[TradingSessionResult]: A list of TradingSessionResult objects containing the results of each simulation setup.
|
|
@@ -88,6 +94,9 @@ def simulate(
|
|
|
88
94
|
# - we need to reset stopwatch
|
|
89
95
|
Stopwatch().reset()
|
|
90
96
|
|
|
97
|
+
if instruments is None:
|
|
98
|
+
instruments = []
|
|
99
|
+
|
|
91
100
|
# - process instruments:
|
|
92
101
|
_instruments, _exchanges = find_instruments_and_exchanges(instruments, exchange)
|
|
93
102
|
|
|
@@ -100,7 +109,9 @@ def simulate(
|
|
|
100
109
|
raise SimulationError(_msg)
|
|
101
110
|
|
|
102
111
|
# - recognize provided data
|
|
103
|
-
data_setup = recognize_simulation_data_config(
|
|
112
|
+
data_setup = recognize_simulation_data_config(
|
|
113
|
+
data, _instruments, open_close_time_indent_secs, aux_data, prefetch_config
|
|
114
|
+
)
|
|
104
115
|
|
|
105
116
|
# - recognize setup: it can be either a strategy or set of signals
|
|
106
117
|
simulation_setups = recognize_simulation_configuration(
|
|
@@ -114,6 +125,7 @@ def simulate(
|
|
|
114
125
|
signal_timeframe=signal_timeframe,
|
|
115
126
|
accurate_stop_orders_execution=accurate_stop_orders_execution,
|
|
116
127
|
run_separate_instruments=run_separate_instruments,
|
|
128
|
+
enable_funding=enable_funding,
|
|
117
129
|
)
|
|
118
130
|
if not simulation_setups:
|
|
119
131
|
logger.error(
|
|
@@ -129,7 +141,7 @@ def simulate(
|
|
|
129
141
|
# - preprocess start and stop and convert to datetime if necessary
|
|
130
142
|
if stop is None:
|
|
131
143
|
# - check stop time : here we try to backtest till now (may be we need to get max available time from data reader ?)
|
|
132
|
-
stop = pd.Timestamp.now(tz="UTC")
|
|
144
|
+
stop = to_utc_naive(pd.Timestamp.now(tz="UTC"))
|
|
133
145
|
|
|
134
146
|
_start, _stop = handle_start_stop(start, stop, convert=pd.Timestamp)
|
|
135
147
|
assert isinstance(_start, pd.Timestamp) and isinstance(_stop, pd.Timestamp), "Invalid start and stop times"
|
|
@@ -204,6 +216,7 @@ def _run_setups(
|
|
|
204
216
|
portfolio_log_freq,
|
|
205
217
|
enable_inmemory_emitter,
|
|
206
218
|
emitter_stats_interval,
|
|
219
|
+
close_data_readers=True,
|
|
207
220
|
)
|
|
208
221
|
for id, setup in enumerate(strategies_setups)
|
|
209
222
|
)
|
|
@@ -219,6 +232,20 @@ def _run_setups(
|
|
|
219
232
|
return successful_reports
|
|
220
233
|
|
|
221
234
|
|
|
235
|
+
def _adjust_start_date_for_min_instrument_onboard(setup: SimulationSetup, start: pd.Timestamp) -> pd.Timestamp:
|
|
236
|
+
"""
|
|
237
|
+
Adjust the start date for the simulation to the onboard date of the instrument with the minimum onboard date.
|
|
238
|
+
"""
|
|
239
|
+
onboard_dates = [
|
|
240
|
+
to_utc_naive(pd.Timestamp(instrument.onboard_date))
|
|
241
|
+
for instrument in setup.instruments
|
|
242
|
+
if instrument.onboard_date is not None
|
|
243
|
+
]
|
|
244
|
+
if not onboard_dates:
|
|
245
|
+
return start
|
|
246
|
+
return max(start, min(onboard_dates))
|
|
247
|
+
|
|
248
|
+
|
|
222
249
|
def _run_setup(
|
|
223
250
|
setup_id: int,
|
|
224
251
|
account_id: str,
|
|
@@ -231,12 +258,18 @@ def _run_setup(
|
|
|
231
258
|
portfolio_log_freq: str,
|
|
232
259
|
enable_inmemory_emitter: bool = False,
|
|
233
260
|
emitter_stats_interval: str = "1h",
|
|
261
|
+
close_data_readers: bool = False,
|
|
234
262
|
) -> TradingSessionResult | None:
|
|
235
263
|
try:
|
|
236
264
|
emitter = None
|
|
237
265
|
emitter_data = None
|
|
238
266
|
if enable_inmemory_emitter:
|
|
239
267
|
emitter = InMemoryMetricEmitter(stats_interval=emitter_stats_interval)
|
|
268
|
+
|
|
269
|
+
# TODO: this can be removed once we add some artificial data stream to move the simulation
|
|
270
|
+
if setup.setup_type in [SetupTypes.SIGNAL, SetupTypes.SIGNAL_AND_TRACKER]:
|
|
271
|
+
start = _adjust_start_date_for_min_instrument_onboard(setup, start)
|
|
272
|
+
|
|
240
273
|
runner = SimulationRunner(
|
|
241
274
|
setup=setup,
|
|
242
275
|
data_config=data_setup,
|
|
@@ -252,7 +285,7 @@ def _run_setup(
|
|
|
252
285
|
level=QubxLogConfig.get_log_level(), custom_formatter=SimulatedLogFormatter(runner.ctx).formatter
|
|
253
286
|
)
|
|
254
287
|
|
|
255
|
-
runner.run(silent=silent)
|
|
288
|
+
runner.run(silent=silent, close_data_readers=close_data_readers)
|
|
256
289
|
|
|
257
290
|
# - service latency report
|
|
258
291
|
if show_latency_report:
|